【问题标题】:Most Efficient Unicode Hash Function for Delphi 2009Delphi 2009 最高效的 Unicode 哈希函数
【发布时间】:2010-11-03 13:12:39
【问题描述】:

我需要 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 代码非常简单,并且只使用位移操作,所以很难相信它不会很快。

【问题讨论】:

  • 在你最后的 HashOf 中,我应该从 1 变为 Length(key)。
  • @gabr:谢谢。我现在看到我写了“跟进”,甚至没有意识到我最终使用了与我的问题相同的功能,除了我在跟进中犯了错误。我会重写的。

标签: delphi unicode assembly hash delphi-2009


【解决方案1】:

ASM 输出并不是算法速度的良好指示。此外,据我所知,这两段代码所做的工作几乎相同。最大的区别似乎是内存访问策略,第一个是使用左滚动而不是等效的指令集(shl | shr——大多数高级编程语言都省略了“滚动”运算符)。后者可能比前者更好。

ASM 优化是一种黑魔法,有时更多的指令比更少的指令执行得更快。

可以肯定的是,对两者进行基准测试并选出获胜者。如果您喜欢第二个的输出但第一个更快,请将第二个的值插入第一个。

rol edx,5 { edx := (edx shl 5) or (edx shr 27)... }

请注意,不同的机器将以不同的方式运行代码,因此如果速度真的很重要,那么请在您计划运行最终应用程序的硬件上对其进行基准测试。我敢打赌,在数兆字节的数据上,差异将是几毫秒的问题——这远远小于操作系统从你身上夺走的时间。


PS。我不相信这个算法会创建均匀分布,这是你明确指出的(你有没有运行直方图?)。您可以查看将this hash function 移植到Delphi。它可能没有上述算法那么快,但它看起来相当快并且也提供了良好的分布。再说一次,我们可能谈论的是兆字节数据的毫秒级差异。

【讨论】:

  • 我不能同意这一点。在现代处理器上,尝试手动优化汇编器几乎已成为过去。
  • 我很欣赏你的想法。我真的不打算疯狂地优化汇编代码。但我想消除明显​​的开销。我的程序一次运行可以调用哈希函数数亿次,因为它几乎用于所有事情
  • @lkessler,这里没有太多需要消除的开销。您可能会发现找出缓存值的位置比在散列函数中挤出几微秒的执行时间得到更大的优化。当您分析您的应用程序并看到您的大部分时间都花在哈希方法上时,有两个选项 - 优化哈希函数(不会再走太远)或弄清楚如何减少调用它。你现在最好的选择是后者。
【解决方案2】:

不久前我们举办了一场不错的小型竞赛,改进了名为“MurmurHash”的哈希;引用维基百科:

它以与众不同而著称 快,通常快两到四倍 比类似的算法,如 FNV、Jenkins 的 lookup3 和 Hsieh 的 SuperFastHash,具有出色的 分布,雪崩行为和 整体抗碰撞性。

您可以下载该比赛的提交here

我们学到的一件事是,有时优化并不能改善每个 CPU 上的结果。我的贡献被调整为在 AMD 上运行良好,但在 Intel 上表现不佳。反过来也发生了(英特尔优化在 AMD 上运行次优)。

所以,正如 Talljoe 所说:衡量您的优化,因为它们实际上可能会损害您的性能!

附带说明:我不同意 Lee 的观点; Delphi 是一个很好的编译器,但有时我看到它生成的代码并不是最优的(即使在编译时打开了所有优化)。例如,我经常看到它清除了之前已经清除了两三个语句的寄存器。或者 EAX 被放入 EBX,只是将其移动并放回 EAX。之类的东西。我只是在这里猜测,但手动优化这种代码肯定会在紧要关头有所帮助。

最重要的是;首先分析你的瓶颈,然后看看是否可以使用更好的算法或数据结构,然后尝试优化 pascal 代码(如:减少内存分配、避免引用计数、终结、try/finally、try/except 块等),然后,仅作为最后的手段,优化汇编代码。

【讨论】:

    【解决方案3】:

    我已经在 Delphi 中编写了两个汇编“优化”函数,或者在微调的 Pascal 和 Borland Assembler 中实现了更多已知的快速哈希算法。第一个是 SuperFastHash 的实现,第二个是由 Tommi Prami 在我的博客上的请求触发的 MurmurHash2 实现,将我的 c# 版本转换为 Pascal 实现。这产生了discussion continued on the Embarcadero Discussion BASM Forums,最终导致了大约 20 个实现(查看latest benchmark suite),最终表明由于 Intel 和 AMD 之间每条指令的循环时间存在很大差异,因此很难选择最佳实现.

    因此,尝试其中一种,但请记住,每次都获得最快可能意味着将算法更改为更简单的算法,这会损害您的分布。微调实现需要花费大量时间,最好创建一个良好的验证和基准测试套件来检查您的实现。

    【讨论】:

    • 戴维:很高兴收到工作人员的来信。我在对 talljoe 的回答的评论中注意到了您的实施,并且 PhiS 指出了讨论。看起来 SuperFastHash 有很多代码,尤其是当您将它与我的问题的 HashOf 函数中的六行 pascal 进行比较时。我想知道是什么让 SuperFastHash 比 HashOf 快,如果它更快,那么快多少?
    • @lkessler:您的问题都指向每个答案中提到的内容,创建一个基准测试程序来模拟您对哈希函数的预期用法,测量速度和分布,您可能会找到原因SuperFastHash/MurmurHash2 可能比 HashOf 慢。对于小字符串(10 个字符),我会 expect HashOf 更快,对于较大的字符串,其他函数具有展开的循环以利用。
    【解决方案4】:

    Delphi/BASM 论坛中有一些您可能感兴趣的讨论。请看以下内容:

    http://forums.embarcadero.com/thread.jspa?threadID=13902&tstart=0

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2010-09-12
      • 2010-11-28
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-11-16
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多