WIN32 之外的字符串太慢?
在我最近参与的一场辩论中,有人说Win64 运行时中的字符串运行速度太慢,根本没法用。在我看来,这简直是夸大其词。Win32 运行时库 (RTL) 确实从FastCode项目的工作中受益匪浅,通常这些例程都使用了极其智能的汇编语言。对于所有其他平台,这些例程通常都使用普通的Object Pascal 语言编写 ,因此无需使用汇编语言。此外,被智能实现所取代的例程也少得多。
一个非常明显的例子是函数Pos,它搜索某个字符串(我称之为Needle)是否可以在一个更大的字符串( )中找到 Haystack。它的Win32实现采用高度优化的汇编语言, 由FastCode项目的Aleksandr Sharahov编写,并由 CodeGear 授权。它的Win64 实现采用纯 Pascal 语言()。但是 的实现 与 的实现并不相同,甚至不相似 !PUREPASCALUnicodeStringAnsiString
的实现比Win32UnicodeString的相同例程慢 。在我的系统上,在Win64中进行搜索大约需要Win32所需时间的 1.8 倍。在Win32上, for 的速度与for差不多(有时甚至略快)。但在Win64上,for所需的时间是 needs 的 2 倍!PosAnsiStringPosUnicodeStringPosAnsiStringPosUnicodeString
如果您查看中的源代码System.pas,您会发现 UnicodeChar版本的优化程度略高(在第 一个中 搜索第一个 Needle,并且仅在找到匹配项时才检查其余部分)。
为了好玩,我把实现的代码改成了UnicodeString,让它能运行在 上。 结果AnsiString比 稍微快了一点,而不是慢了两倍。我很奇怪,为什么 的实现不像我那样 直接使用 的代码呢?如果我是个多疑的人,我会认为这是故意的,目的是降低它的可用性,从而贬低它。System.PosUnicodeStringSystem.pasAnsiStringUnicodeStringAnsiString
但即使这样,也还有改进的余地。我为自己的例程写了三个实现,一个用于AnsiString,一个用于UnicodeString,一个用于TBytes(很多人抱怨TBytes缺少类似的东西Pos,这就是为什么他们保留了使用字符串存储二进制数据的糟糕习惯——<shudder>——我想消除这个愚蠢的争论)。
代码
下面是我的函数的代码RVPosExA(值得一提的是:现在PosEx和Pos之间已经没有区别了:两者都有完全相同的功能和签名):
function RVPosExA(const Needle, Haystack: AnsiString; Offset: Integer = 1): Integer; type PUInt32 = ^UInt32; PUInt16 = ^UInt16; {$IFNDEF CPU32BITS} var LNeedleTip: UInt32; PNeedle: PAnsiChar; PHaystack, PEnd: PAnsiChar; LLenNeedle: Integer; LCmpMemOffset: Integer; {$ENDIF} begin {$IFDEF CPU32BITS} // FastCode (asm) implementation. Result := System.Pos(Needle, Haystack, Offset); {$ELSE} if Offset - 1 + Length(Needle) > Length(Haystack) then Exit(0); Result := 0; PHaystack := PAnsiChar(Haystack) + Offset - 1; PEnd := PHaystack + Length(Haystack) - Length(Needle) + 1; case Length(Needle) of 0: Exit(0); 1: begin LNeedleTip := PByte(Needle)^; while PHaystack < PEnd do if PByte(PHaystack)^ = LNeedleTip then Exit(PHaystack - PAnsiChar(Haystack) + 1) else Inc(PHaystack); Exit(0); end; 2: begin LNeedleTip := PUInt16(Needle)^; while PHaystack < PEnd do if PUInt16(Haystack)^ = LNeedleTip then Exit(PHayStack - PAnsiChar(Haystack) + 1) else Inc(PHaystack); Exit(0); end; 3: begin LNeedleTip := PUInt32(Needle)^; // if Needle is length 3, then top byte // is the #0 terminator while PHaystack < PEnd do if ((PUInt32(Haystack)^ xor LNeedleTip) and $FFFFFF) = 0 then Exit(PHaystack - PAnsiChar(Haystack) + 1) else Inc(PHaystack); Exit(0); end; 4: begin LNeedleTip := PUInt32(Needle)^; while PHaystack < PEnd do if PUInt32(Haystack)^ = LNeedleTip then Exit(PHaystack - PAnsiChar(Haystack) + 1) else Inc(PHaystack); Exit(0); end; else begin LCmpMemOffset := SizeOf(UInt32) div SizeOf(AnsiChar); PNeedle := PAnsiChar(Needle) + LCmpMemOffset; LLenNeedle := Length(Needle) - LCmpMemOffset; LNeedleTip := PUInt32(Needle)^; while PHaystack < PEnd do if (PUInt32(PHaystack)^ = LNeedleTip) and CompareMem(PHaystack + LCmpMemOffset, PNeedle, LLenNeedle) then Exit(PHaystack - PAnsiChar(Haystack) + 1) else Inc(PHaystack); end; end; {$ENDIF} end;
如你所见,在Win32下,它会直接跳转到 System.Pos,因为无论如何这都是最快的。但在所有其他平台上,它会Haystack按 4 字节顺序搜索(如果 Needle大于 4 个元素),如果找到了,则会使用 继续搜索剩余部分CompareMem。
定时
以下是稍微重新格式化的测试程序的输出(我将WIN32和WIN64列放在一起,以节省空间):
Pos(Needle, Haystack: <sometype>; Offset: Integer) 的不同版本:Integer 其中 <sometype> 是 UnicodeString、AnsiString 或 TBytes 使用 Haystack 长度 50、200、3000、4000 和 300000 进行测试 针长 1、3、8 和 20 5 * 4 * 2000 = 40000 个循环 WIN64 WIN32 Unicode字符串 Unicode字符串 ------------- ------------- 系统位置:2428 毫秒 系统位置:1051 毫秒 StrUtils.PosEx:2258 毫秒 StrUtils.PosEx:1070 毫秒 RVPosExU:1071 毫秒 RVPosExU:1050 毫秒 AnsiString AnsiString ---------- ---------- 系统位置:4956 毫秒 系统位置:1046 毫秒 AnsiStrings.PosEx:4959 毫秒 AnsiStrings.PosEx:1051 毫秒 OrgPosA:5129 毫秒 OrgPosA:5712 毫秒 PosUModForA:1958 毫秒 PosUModForA:3744 毫秒 RVPosExA:1322 毫秒 RVPosExA:1086 毫秒 太字节 太字节 ------ ------ RVPosEXB:998 毫秒 RVPosEXB:2754 毫秒 Haystack:500000000 个 ASCII 字符或字节的随机字符串 Needle:Haystack 的最后 10 个字符 = 'WRDURJVDFA' WIN64 WIN32 Unicode字符串 Unicode字符串 ------------- ------------- 系统位置:847 毫秒 系统位置:421 毫秒 Strutils.PosEx:827 毫秒 Strutils.PosEx:414 毫秒 RVPosExU:421 毫秒 RVPosExU:438 毫秒 AnsiString AnsiString ---------- ---------- 系统位置:1735 毫秒 系统位置:428 毫秒 AnsiStrings.PosEx:1831 毫秒 AnsiStrings.PosEx:428 毫秒 OrgPosA:1749 毫秒 OrgPosA:2687 毫秒 PosUModForA:708 毫秒 PosUModForA:1525 毫秒 RVPosExA:368 毫秒 RVPosExA:423 毫秒 RvPosExA(,,偏移): 200 毫秒 RvPosExA(,,偏移): 220 毫秒 太字节 太字节 ------ ------ RVPosExB(TBytes):385 毫秒 RVPosExB(TBytes):1095 毫秒
例程RVPosExA、RVPosExU和分别RVPosExB是我对AnsiString、UnicodeString和的实现。是的原始代码,而是的原始PUREPASCAL代码,经过了 的修改。TBytesOrgPosAPosAnsiStringPosUModForAPosUnicodeStringAnsiString
正如您所见,该PosUModForA例程的速度几乎是相当愚蠢的两倍OrgPosA,并且在WIN32中,RVPosEx<A/U/B>实现速度比其他例程更快。
我没有验证,但FastCode项目的纯 Pascal 版本很可能更快。但对我来说,这个实现只是一个开始,也证明了通过一些简单的优化,字符串例程可以变得更快。或许,有一天,Embarcadero 会更多地采用 FastCode 项目中的纯 Pascal 代码。
可以从我的网站下载例程的代码和产生上述输出的程序。
4条评论:
https://rvelthuis.blogspot.com/2018/01/strings-on-other-platforms-than-32-bit.html

浙公网安备 33010602011771号
你可以在这里找到 Fastcoders PurePascal 版本的代码和时间:https://stackoverflow.com/a/20947429/576719
以及 QP 的链接:https://quality.embarcadero.com/browse/RSP-13687
我已经放弃 Delphi 了。RTL 不会再有任何改进,而且每个新版本都会引入太多重大更改。我想把时间花在编程上,而不是测试 RTL 的稳定性。
重大变更?我没注意到 Delphi 的最新版本中有多少重大变更(Unicode 除外)。而且 RTL 在过去几年里确实有所改进(虽然可能不是你想要的),我估计还会有更多改进。
Unicode 确实易于管理。但 NextGen 编译器确实引入了一些其他重大更改,其中最大的就是 ARC 和禁用 AnsiString(即使最近部分重新引入)。这对我的代码库来说真是太麻烦了。
我怀疑 Embarcadero 是否认为 ARC 是一项突破性的变化,尤其是考虑到它仅在新平台上实现。AnsiString 也是如此:它们从未在这些平台上存在过,实际上,AnsiString(Ansi 代码页系统)或多或少是 Windows 独有的功能。UTF8String 则并非如此,因为 UTF-8 在所有平台上都是可识别的。