大约3年前,我在关于BloomFilter容量的研究一文中提到想研究一下各种HASH算法的速度。时至今日,一直没有动手去做。自我安慰地说,Premature Optimization是没有必要的,可能大多数HASH算法都差不多吧。但最近碰上了一个O(2n)问题,为解决它,我必须去设计一个尽可能高效的BloomFilter和HASH对象。HASH算法的选择就变得相当重要了。
首先,我做了一个Literature Review,研究了以下资料:
- http://en.wikipedia.org/wiki/Jenkins_hash_function
-
- http://www.burtleburtle.net/bob/hash/doobs.html
- http://www.burtleburtle.net/bob/hash/spooky.html
- http://en.wikipedia.org/wiki/Fowler_Noll_Vo_hash
-
- http://www.isthe.com/chongo/tech/comp/fnv/index.html
- http://en.wikipedia.org/wiki/CityHash
- http://en.wikipedia.org/wiki/MurmurHash
- http://programmers.stackexchange.com/a/145633/108318
- http://www.drdobbs.com/parallel/fast-parallelized-crc-computation-using/229401411
- http://stackoverflow.com/questions/17645167/implementing-sse-4-2s-crc32c-in-software
根据上述资料,备选的HASH可能是:
- CityHash
- SpookyHash
- MurmurHash3
- FNV Hash
- CRC32/SSE4.2
以上绝大多数HASH的“官方”版本是C或者C++的。但我使用的语言是FreePascal。重写这些HASH是个不小的工作量。由于我开始入手研究的是lookup3这个HASH(也就是Spooky作者写的一个早期版本),加上FP已经内建了CRC32支持,我选择了以下几个算法进行测试:
- lookup3(我这次研究的出发点)
- crc32(FreePascal内置支持)
- fnv1a(改写足够简单)
- murmur2(以前有现成的Pascal代码)
- crc128(FreePascal内置支持,可能需要128bit的HASH)
- md5(同上)
- sha1(同上)
执行循环一千万次后的结果如下(单位:秒):
- lookup3: 3.027
- crc32: 2.810
- fnv1a: 4.185
- murmur2: 2.128
- crc128: 6.028
- md5: 5.698
- sha1: 7.692
对以上测试结果,我的解读如下:
- 测试结果只能作相对比较,因为测试程序的编写、测试数据的长度等等都是非常随意的。我的这次测试使用的是10个长整数,也就是40个字节的数据,共一千万个,使用EpikTimer对每次哈希运算进行计时然后累加。这样做可能引入EpikTimer本身带来的损耗,但不影响结论的相对比较价值。
- 32-bit的Hash中Murmur2的性能最好,依此推断,Murmur3的性能可能还要好一点。
- CRC32的性能相当不错,从简便性上看,它应该是最优选择了,因为FP天然就支持了。
- FP的CRC32使用了查表法,基本上是软件所能达到的CRC32最好性能了。但肯定比用SSE4.2的硬件指令的要慢。另外听说CityHash也会利用硬件特性来提速,这里就不加以测试了。
- 128-bit的算法“单位速度”明显好于32-bit的。例如MD5算法按32-bit的速度算只需1.507秒,远高于任何32-bit算法。在BloomFilter应用中可以将大HASH切分成若干小HASH使用,因此应该尽可能选择“单位速度”高的算法。
- 所测试的算法中单位速度最高的几个依次是:MD5、CRC128、SHA1、Murmur2、CRC32。综合考虑性能和使用的方便性、各种场合下的不同需求等,应使用FP内建支持的MD5、SHA1、Murmur2(或Murmur3)三种算法,若对速度要求不是很高,也可以使用CRC32。
后记
本文发布后一天,我继续进行HASH性能的研究,找到了smhasher项目。虽然它包含了详细的测试数据,但我还是想自己测一下。于是改写了它里面的MurmurHash3代码。得出的结果如下:
- Murmur3的32bit版本性能介乎CRC32和Murmur2之间,为2.5秒多一点。
- Murmur3的128bit版本性能惊人,比32bit更快一点,也就是说单位速度达32bit版本的4到5倍,可惜我改写的代码中还有BUG,和C的原版计算出来的值不同。暂时不能用。
- 从这次研究我的经验教训是,测试程序必须自己写而不能依赖他人公开的测试。以我这次为例,性能和公开可以查询到的结果有比较大的不同的原因我认为主要不是Pascal和C的区别,而是数据特性和统计方法所决定的。不管这种猜测是否正确,最后反应到实际应用中还是以硬的耗时指标说话,而不是每个hash运算消耗多少个CPU指令循环这样高深而学术化的衡量。
此次测试后,我仍然决定在未来的项目中优先使用FP内置的CRC32/MD5/SHA1系列算法。但会考虑研究测试一下基于SSE4.2指令集的版本,看看是否有性能优势。