Delphi:为什么二进制字符串比较运算符(=)不使用SameStr?

14
众所周知,在Delphi中,对于变量var S1, S2: string,使用SameStr(S1, S2)S1 = S2更快。

(当然,SameText(S1, S2)AnsiLowerCase(S1) = AnsiLowerCase(S2)要快得多。)

但据我所了解,SameStr(S1, S2)S1 = S2的功能完全相同,所以我不禁想知道为什么Delphi编译器在使用=运算符测试字符串相等时没有使用SameStr代码。毫无疑问,这肯定有原因吧?

一些基准测试

一个微不足道的程序:

program Project1;

{$APPTYPE CONSOLE}

uses
  SysUtils,
  RejbrandCommon;

const
  N = 1000000;

var
  Strings1, Strings2: StringArray;
  i: integer;
  b: {dummy }boolean;

procedure CreateRandomStringArrays;
var
  i: integer;
begin
  SetLength(Strings1, N);
  SetLength(Strings2, N);
  for i := 0 to N - 1 do
  begin
    Strings1[i] := RandomString(0, 40);
    Strings2[i] := RandomString(0, 40);
  end;
end;

begin

  CreateRandomStringArrays;

  StartClock;
  for i := 0 to N - 1 do
    if Strings1[i] = Strings2[i] then
      b := not b;
  StopClock;
  OutputClock;

  StartClock;
  for i := 0 to N - 1 do
    if SameStr(Strings1[i], Strings2[i]) then
      b := not b;
  StopClock;
  OutputClock;

  Pause;

end.

在哪里

function RandomString(const LowerLimit: integer = 2; const UpperLimit: integer = 20): string;
var
  N, i: integer;
begin
  N := RandomRange(LowerLimit, UpperLimit);
  SetLength(result, N);
  for i := 1 to N do
    result[i] := RandomChar;
end;

并且内联的

function RandomChar: char;
begin
  result := chr(RandomRange(ord('A'), ord('Z')));
end;

"clock"函数只是对QueryPerformanceCounter, QueryPerformanceFrequency, 和 Writeln 进行了包装,输出结果。

2.56599325762716E-0002
1.24310093156453E-0002
ratio ~ 2.06
如果我们要比较的两个字符串长度相差很大,那么它们之间的差异会更加明显。我们会尝试
Strings1[i] := RandomString(0, 0); // = '';
Strings2[i] := RandomString(0, 40);

获取并获得

1.81630411160156E-0002
4.44662043198641E-0003
ratio ~ 4.08
所以为什么编译器在为 S1 = S2 生成汇编代码时不使用 SameStr 代码呢?
更新
阅读了 Cosmin Prund 的出色回答后,我忍不住设置了。
Strings1[i] := RandomString(40, 40);
Strings2[i] := RandomString(40, 40);

生成长度相等的字符串。

2.74783364614126E-0002
1.96818773095322E-0002
ratio ~ 1.40

嗯... SameStr 仍然胜出...

我的规格

CPU Brand String: Intel(R) Core(TM) i7 CPU         870  @ 2.93GHz
Memory: 6 GB
OS: Windows 7 Home Premium (64-bit)
Compiler/RTL: Delphi 2009

更新

(请查看Cosmin Prund的回答下面的评论),似乎=运算符在D2009和D2010之间发生了变化。有人可以证实吗?


如果你想运行一些额外的测试,你可能需要修复你的 RandomString 函数(Result[N] -> Result[I])。 - Ken Bourassa
@Ken Bourassa:哦,发现得真好! - Andreas Rejbrand
4个回答

19

回答

这完全取决于您生成随机字符串的方式。我使用了修改过的代码,因为很少有人拥有RejbrandCommon单元,并且因为我想使用Excel来完成我的分析(并制作漂亮的图表)。

代码(跳过代码以查看一些结论):

program Project3;

{$APPTYPE CONSOLE}

uses
  SysUtils, Windows;

const
  StringsNumber = 2000000;

var
  Strings1, Strings2: array of string;
  StrLen: integer;
  b: {dummy }boolean;

function RandomString(MinLen, MaxLen:Integer):string;
var N, i:Integer;
begin
  N := MinLen + Random(MaxLen-MinLen);
  Assert(N >= MinLen); Assert(N <= MaxLen);
  SetLength(Result, N);
  for i:=1 to N do
    Result[i] := Char(32 + Random(1024)); // Random Unicode Char
end;

procedure CreateRandomStringArrays(StrLen:Integer);
var
  i: integer;
  StrLen2:Integer;
begin
  SetLength(Strings1, StringsNumber);
  SetLength(Strings2, StringsNumber);
  for i := 0 to StringsNumber - 1 do
  begin
    StrLen2 := StrLen + Random(StrLen div 2);
    Strings1[i] := RandomString(StrLen, StrLen2);
    StrLen2 := StrLen + Random(StrLen div 2);
    Strings2[i] := RandomString(StrLen, StrLen2);
  end;
end;

var C1, C2, C3, C4:Int64;

procedure RunTest(StrLen:Integer);
var i:Integer;
begin
  CreateRandomStringArrays(StrLen);

  // Test 1: using equality operator
  QueryPerformanceCounter(C1);
  for i := 0 to StringsNumber - 1 do
    if Strings1[i] = Strings2[i] then
      b := not b;
  QueryPerformanceCounter(C2);

  // Test 2: using SameStr
  QueryPerformanceCounter(C3);
  for i := 0 to StringsNumber - 1 do
    if SameStr(Strings1[i], Strings2[i]) then
      b := not b;
  QueryPerformanceCounter(C4);

  // Results:
  C2 := C2 - C1;
  C4 := C4 - C3;
  WriteLn(IntToStr(StrLen) + #9 + IntToStr(C2) + #9 + IntToStr(C4));
end;

begin

  WriteLn('Count'#9'='#9'SameStr');
  for StrLen := 1 to 50 do
    RunTest(StrLen);

end.

我修改了CreateRandomStringArrays例程,使其接受一个StrLen参数,以便我可以在循环中运行多个类似测试。我改用QueryPerformanceCounter直接计时,并使用制表符分隔的方式WriteLn结果,以便我可以将其复制/粘贴到Excel中。在Excel中,结果如下所示:

StrLen  =   SameStr
1   61527   69364
2   60188   69450
3   72130   68891
4   78847   85779
5   77852   78286
6   83612   88670
7   93936   96773

然后我对数据进行了一些归一化处理。在每行数据中,将最大值设置为“1”,并将其他值设置为1的百分比。结果如下:

StrLen  =   SameStr
1   0,88    1
2   0,86    1
3   1   0,95
4   0,91    1
5   0,99    1
6   0,94    1
7   0,97    1

然后我开始尝试使用CreateRandomStringArrays例程运行多个测试。

这是原始情况的绘图(CreateRandomStringArrays生成随机长度的字符串,从长度1到X轴上的任意长度)。蓝色是“=”运算符的结果,红色是“SameStr”的结果,较低的值更好。显然,对于长度大于10个字符的字符串,“SameStr”()有优势。

alt text http://fisiere.sediu.ro/PentruForumuri/V1_1_to_maxlen.png

接下来的测试,使CreateRandomStringArrays返回长度相等的字符串。字符串内容仍然是完全随机的,但是字符串的长度等于X轴上的任何长度。这时“=”运算符显然更有效:

alt text http://fisiere.sediu.ro/PentruForumuri/V1_equal_strings.png

现在真正的问题是,在实际代码中,字符串相等的概率是多少?同样,字符串之间的差异有多大,才能让SameStr()开始占据优势?下一个测试中,我构建了两个字符串,第一个的长度为StrLen(X轴上的数字),第二个字符串的长度为StrLen + Random(4)。同样,“=”运算符更有效:

alt text http://fisiere.sediu.ro/PentruForumuri/V1_rnd_p4.png

下一个测试中,我有两个长度为StrLen + Random(StrLen div 10)的字符串,“=”运算符更有效。

alt text http://fisiere.sediu.ro/PentruForumuri/V1_rnd_pm_10p.png

......最后一个测试,字符串长度为+/-50%。公式:StrLen + Random(StrLen div 2)。SameStr()获胜:

alt text http://fisiere.sediu.ro/PentruForumuri/V1_rnd_pm_50p


1
非常好的分析。我一直知道SameStr在处理不同长度的字符串时特别出色,但让我惊讶的是对于相同长度的字符串,实际上“=”更快。每天都能学到新东西! - Andreas Rejbrand
1
嗯...我无法再现“=”在长度相等的字符串中获胜的结果... - Andreas Rejbrand
@Andreas:看到汇编代码会很有趣,这样我们就能知道区别了!(顺便问一下,两者是否使用相同的构建选项,例如优化?) - Remko
@Remko:我已经尝试过在调试和发布模式下构建该项目。我使用D2009来构建Cosmin Prund的DRP:http://privat.rejbrand.se/streqlen.exe。请随意与Cosmin Prund的EXE进行比较。 - Andreas Rejbrand
很不幸,上面答案中显示结果的内联图片已经无法访问。 - Ian Goldby
显示剩余4条评论

4

SameStr有一个可选的第三个参数:LocaleOptions。如果省略第三个参数,您将获得类似于“=”的行为:区分大小写、与语言环境无关的比较。

您可能会认为这与二进制比较相同,但实际上并不是。

自D2009 Delphi字符串除了长度和引用计数外还有一个“代码页”负载。

  StrRec = packed record
    codePage: Word;
    elemSize: Word;
    refCnt: Longint;
    length: Longint;
  end;

当你执行 String1 = String2 时,你告诉编译器忽略关于字符串的所有信息,只进行二进制比较(它使用 UStrEqual 进行比较)。
当你执行 SameStr 或 CompareStr(SameStr 使用 CompareStr)时,Delphi 首先检查字符串是否为 Unicode(UTF-16LE),如果不是,则在执行实际操作之前进行转换。
当你查看 CompareStr 的实现时,可以看到这一点(没有第三个参数的函数)。在进行初始优化后,该函数会检查参数是否为 Unicode 字符串,如果不是,则使用 UStrFromLStr 进行转换。
更新:
实际上,UStrEqual(通过 UStrCmp)也会进行转换,就像 CompareStr 一样,它会查看字符串的 elemSize,以确定它们是否为 Unicode,并在它们不是 Unicode 时进行转换。
因此,编译器为什么不在“=”运算符中使用 SameStr(CompareStr)的原因我还不清楚。我唯一能想到的是它与用于 '=' 比较 AnsiStrings 的 LStrEqual 有一个很好的类比。我想只有编译器的人知道。
抱歉浪费了您的时间。不过我会保留这个答案,这样其他人就不必走这条调查路线了。

嗯...但这是否意味着“=”比SameStr更快呢? - Andreas Rejbrand
@Anders。不,看我的更新内容。'='确实可以进行转换。它只是使用了一个不同的比较循环,我想这远不及CompareStr中的那个好。 - Marjan Venema
不算什么大事,但我的名字实际上是“Andreas”。 - Andreas Rejbrand

0

未来的一些测试:

  • Delphi Seattle更新1
  • i5-2500k @ 4.3Ghz
  • 10亿次迭代
  • 比较2个17个字符长的字符串

不同文本:
// = -> 1890 毫秒
// CompareText -> 4500 毫秒
// CompareStr -> 2130 毫秒

相同文本:
// = -> 1890 毫秒
// CompareText -> 10900 毫秒
// CompareStr -> 1895 毫秒

结论:= 在所有情况下都更快,但是CompareStr对于相同的文本几乎和=一样快。此外,当处理Ansi字符串时,CompareText/Str似乎要慢得多。


0

在我的系统上,“=”比SameStr快。

使用“RandomString(0,0)”示例后,SameStr的速度确实变快了(约20%)。但是,如果第二个字符串被设置为空字符串,则性能几乎相同。经过更多测试,似乎长度不同并不影响性能差异,而是空字符串导致的。

Cosmin Prund刚刚发布了一份更全面的分析...

需要注意的一件事是,对于这么小的函数(数百万次测试只需几毫秒),实际运行代码的处理器可能会产生很大的差异。ASM代码可能对一个处理器的BPU更友好...或者某些指令在不同的CPU上运行更有效率。数据对齐可能会影响它。缓存未命中会影响性能。这些只是硬件级别上可能影响最终性能的一些例子。

值得一提的是,我所做的测试是在Phenom X4 CPU上进行的。


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