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(值得一提的是:现在PosExPos之间已经没有区别了:两者都有完全相同的功能和签名):

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

定时

以下是稍微重新格式化的测试程序的输出(我将WIN32WIN64列放在一起,以节省空间):

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 毫秒

例程RVPosExARVPosExU分别RVPosExB是我对AnsiStringUnicodeString和的实现。是原始代码,而是的原始PUREPASCAL代码,经过了 的修改TBytesOrgPosAPosAnsiStringPosUModForAPosUnicodeStringAnsiString

正如您所见,该PosUModForA例程的速度几乎是相当愚蠢的两倍OrgPosA,并且在WIN32中,RVPosEx<A/U/B>实现速度比其他例程更快。

我没有验证,但FastCode项目的纯 Pascal 版本很可能更快。但对我来说,这个实现只是一个开始,也证明了通过一些简单的优化,字符串例程可以变得更快。或许,有一天,Embarcadero 会更多地采用 FastCode 项目中的纯 Pascal 代码。

可以从我的网站下载例程的代码和产生上述输出的程序

 

4条评论:

  1.  

    你可以在这里找到 Fastcoders PurePascal 版本的代码和时间:https://stackoverflow.com/a/20947429/576719

    以及 QP 的链接:https://quality.embarcadero.com/browse/RSP-13687

    我已经放弃 Delphi 了。RTL 不会再有任何改进,而且每个新版本都会引入太多重大更改。我想把时间花在编程上,而不是测试 RTL 的稳定性。

     
  2.  

    重大变更?我没注意到 Delphi 的最新版本中有多少重大变更(Unicode 除外)。而且 RTL 在过去几年里确实有所改进(虽然可能不是你想要的),我估计还会有更多改进。

     
     
  3.  

    Unicode 确实易于管理。但 NextGen 编译器确实引入了一些其他重大更改,其中最大的就是 ARC 和禁用 AnsiString(即使最近部分重新引入)。这对我的代码库来说真是太麻烦了。

     
     
  4.  

    我怀疑 Embarcadero 是否认为 ARC 是一项突破性的变化,尤其是考虑到它仅在新平台上实现。AnsiString 也是如此:它们从未在这些平台上存在过,实际上,AnsiString(Ansi 代码页系统)或多或少是 Windows 独有的功能。UTF8String 则并非如此,因为 UTF-8 在所有平台上都是可识别的。

  5.  

https://rvelthuis.blogspot.com/2018/01/strings-on-other-platforms-than-32-bit.html

posted @ 2015-01-06 19:57  findumars  Views(251)  Comments(0)    收藏  举报