Delphi 2009最高效的Unicode哈希函数

9
我需要一个最快的哈希函数,在Delphi 2009中,它可以从Unicode字符串创建哈希值,并且能够在桶中分布得相对均匀。
我最初使用了GpStringHash中Gabr的HashOf函数。
function HashOf(const key: string): cardinal;
asm
  xor edx,edx     { result := 0 }
  and eax,eax     { test if 0 }
  jz @End         { skip if nil }
  mov ecx,[eax-4] { ecx := string length }
  jecxz @End      { skip if length = 0 }
@loop:            { repeat }
  rol edx,2       { edx := (edx shl 2) or (edx shr 30)... }
  xor dl,[eax]    { ... xor Ord(key[eax]) }
  inc eax         { inc(eax) }
  loop @loop      { until ecx = 0 }
@End:
  mov eax,edx     { result := eax }
end; { HashOf }

但我发现这不能从Unicode字符串中产生好的数字。我注意到Gabr的例程没有更新到Delphi 2009。
然后我在Delphi 2009的SysUtils中发现了HashNameMBCS,并将其翻译为以下简单的函数(其中“string”是Delphi 2009 Unicode字符串):
function HashOf(const key: string): cardinal;
var
  I: integer;
begin
  Result := 0;
  for I := 1 to length(key) do
  begin
    Result := (Result shl 5) or (Result shr 27);
    Result := Result xor Cardinal(key[I]);
  end;
end; { HashOf }

我认为这很不错,直到我查看CPU窗口并看到它生成的汇编代码:

Process.pas.1649: Result := 0;
0048DEA8 33DB             xor ebx,ebx
Process.pas.1650: for I := 1 to length(key) do begin
0048DEAA 8BC6             mov eax,esi
0048DEAC E89734F7FF       call $00401348
0048DEB1 85C0             test eax,eax
0048DEB3 7E1C             jle $0048ded1
0048DEB5 BA01000000       mov edx,$00000001
Process.pas.1651: Result := (Result shl 5) or (Result shr 27);
0048DEBA 8BCB             mov ecx,ebx
0048DEBC C1E105           shl ecx,$05
0048DEBF C1EB1B           shr ebx,$1b
0048DEC2 0BCB             or ecx,ebx
0048DEC4 8BD9             mov ebx,ecx
Process.pas.1652: Result := Result xor Cardinal(key[I]);
0048DEC6 0FB74C56FE       movzx ecx,[esi+edx*2-$02]
0048DECB 33D9             xor ebx,ecx
Process.pas.1653: end;
0048DECD 42               inc edx
Process.pas.1650: for I := 1 to length(key) do begin
0048DECE 48               dec eax
0048DECF 75E9             jnz $0048deba
Process.pas.1654: end; { HashOf }
0048DED1 8BC3             mov eax,ebx

这段代码似乎比Gabr的代码包含更多的汇编代码。
时间很重要。我是否可以做些什么来改进我编写的Pascal代码或者生成的汇编代码?

跟进。

最终我选择了基于SysUtils.HashNameMBCS的HashOf函数。它似乎为Unicode字符串提供了良好的哈希分布,并且速度相当快。

是的,生成了很多汇编代码,但是生成它的Delphi代码非常简单,只使用位移操作,所以很难相信它不会很快。


在你的最终哈希中,我应该从1到key的长度进行操作。 - gabr
@gabr:谢谢。我现在明白了,原来我写的是“后续”,甚至没有意识到我用的是我问题所涉及的相同函数,只是在我的后续中犯了错误。我会重新修改的。 - lkessler
4个回答

9
ASM输出并不是算法速度的好指标。从我所看到的来看,这两段代码几乎在做相同的工作。最大的区别似乎在于内存访问策略,前者使用roll-left而不是等价的一组指令(shl | shr - 大多数高级编程语言省略了"roll"操作符)。后者可能比前者更适合流水线处理。
ASM优化是黑魔法,有时执行更多的指令比执行更少的指令更快。
要确保,对两者进行基准测试并选择胜者。如果你喜欢第二个的输出但第一个更快,请将第二个的值插入第一个中。
rol edx,5 { edx := (edx shl 5) or (edx shr 27)... }

请注意,不同的机器将以不同的方式运行代码,因此如果速度确实很重要,则应在计划运行最终应用程序的硬件上对其进行基准测试。我敢打赌,在超过兆字节的数据上,差异只是毫秒级别 - 这比操作系统从您这里拿走的时间少得多。

附注:我不确定这个算法是否能够创建均匀的分布,这是你明确提出的问题(你是否运行了直方图?)。您可以考虑将 此哈希函数 移植到 Delphi。它可能不像上述算法那样快,但它似乎非常快且能够提供良好的分布。同样,我们可能在处理数兆字节数据时只会有几毫秒的差异。


1
我非常赞同这一点。在现代处理器上,试图手动优化汇编语言几乎已经成为过去式,如果不是的话。 - Lee
我很欣赏你的想法。我并不打算试图疯狂优化汇编代码。但我希望消除明显的开销。我的程序运行一次可以调用哈希函数数亿次,因为它几乎用于所有事情。 - lkessler
2
@lkessler,这里没有太多的开销可以消除。你可能会发现,在缓存值的地方找到更大的优化空间比从哈希函数中挤出几个微秒的执行时间更有用。当你分析你的应用程序并发现大部分时间都花在哈希方法上时,有两种选择——优化哈希函数(已没有太多优化空间),或者想办法尽量少地调用它。你现在最好的选择是后者。 - Talljoe
1
我发现了这个:http://landman-code.blogspot.com/2008/06/superfasthash-from-paul-hsieh.html - lkessler

5
我在Delphi中编写了两个汇编“优化”函数,或者更准确地说是在经过微调的Pascal和Borland Assembler中实现了已知的快速哈希算法。第一个是SuperFastHash的实现,第二个是MurmurHash2的实现,由Tommi Prami在我的博客上要求将其C#版本转换为Pascal实现而触发。这引发了一个讨论会,在Embarcadero Discussion BASM Forums上继续进行,最终产生了约20种实现(请检查最新基准套件),其中显示由于英特尔和AMD之间指令周期时间的巨大差异,很难选择最佳实现。因此,请尝试其中一种,但请记住,每次都获得最快的可能意味着将算法更改为更简单的算法,这将损害你的分布。微调实现需要大量时间,并创建一个良好的验证和基准测试套件以检查你的实现。

Davy:很高兴听到做这项工作的人的消息。我在评论中提到了您的实现,PhiS指出了讨论。看起来SuperFastHash有很多代码,特别是当您将其与我的问题中HashOf函数的六行Pascal进行比较时。我想知道是什么使SuperFastHash比HashOf更快,如果它更快,那么速度快多少? - lkessler
@lkessler:你的问题都指向了每个答案中提到的内容,创建一个基准测试程序来模拟哈希函数的预期使用情况,同时测量速度和分布,你可能会发现为什么SuperFastHash/MurmurHash2比HashOf慢。对于小字符串(10个字符),我预计 HashOf 会更快,对于较大的字符串,其他函数有展开循环以利用优势。 - Davy Landman

5
我们之前举办了一个小型比赛,改进了一个叫做 "MurmurHash" 的哈希算法;引用维基百科的话:
它因其非常快而著名,通常比类似的算法(如FNV、Jenkins的lookup3和Hsieh的SuperFastHash)快两到四倍,并具有出色的分布、雪崩行为和整体碰撞抵抗力。
您可以在这里下载该比赛的提交内容。
我们学到的一件事是,有时优化并不会在每个CPU上都改善结果。我的贡献被调整为在AMD上运行良好,但在Intel上表现不佳。反过来也发生了(Intel优化在AMD上运行次优)。
所以,正如Talljoe所说:要测量您的优化,因为它们可能会对性能产生负面影响!
作为一个旁注:我不同意Lee的观点;Delphi是一个很好的编译器,但有时我看到它生成的代码并不是最优的(即使打开了所有优化)。例如,我经常看到它清除已经在两三个语句之前清除过的寄存器。或者将EAX放入EBX中,然后将其移位并重新放回EAX。这样的事情。我只是猜测,但手动优化这种代码肯定会在紧急情况下有所帮助。
总之,首先分析您的瓶颈,然后看看是否可以使用更好的算法或数据结构,然后尝试优化Pascal代码(例如:减少内存分配,避免引用计数,终止,try / finally,try / except块等),然后,仅作为最后的手段,优化汇编代码。

4

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接