Delphi XE2 64位版本的字符串例程运行时非常缓慢

26

我正在将一些应用程序从32位Delphi移植到64位,这些应用程序进行了大量的文本处理,并注意到处理速度发生了极端变化。对一些过程进行了一些测试,例如,与32位编译相比,这个过程在64位上需要的时间已经超过了200%(2000+毫秒与~900相比)。

这种情况正常吗?

function IsStrANumber(const S: AnsiString): Boolean;
var P: PAnsiChar;
begin
  Result := False;
  P := PAnsiChar(S);
  while P^ <> #0 do begin
    if not (P^ in ['0'..'9']) then Exit;
    Inc(P);
  end;
  Result := True;
end;

procedure TForm11.Button1Click(Sender: TObject);
Const x = '1234567890';
Var a,y,z: Integer;
begin
  z := GetTickCount;
  for a := 1 to 99999999 do begin
   if IsStrANumber(x) then y := 0;//StrToInt(x);
  end;
  Caption := IntToStr(GetTickCount-z);
end;

如果您使用 StrToInt(x),是否会出现相同的问题? - Toby Allen
你做过其他不涉及底层指针操作的测试吗? - Frank Schmitt
2
是的,在循环中只执行StrToInt:2246毫秒对比1498毫秒(64/32)。除此之外,我移植的一个大型应用程序有一个基准测试来测试处理速度(它通过数百个字符串操作子例程传递一些文本),64位的处理时间几乎是32位的两倍。 - hikari
我建议你进行一些测试,特别是将变量转换为in64或longint类型。 - Pieter B
Int64/NativeInt 仍然没有区别。 - hikari
6个回答

35

目前还没有解决方案,因为64位的大多数字符串例程代码都是使用PUREPASCAL编译的,也就是说,它是纯Delphi,没有汇编语言,而32位的许多重要字符串例程代码是由FastCode项目完成的,使用汇编语言。

目前,64位中还没有FastCode的等价物,我认为开发团队将尝试消除汇编语言,特别是因为他们正在转向更多平台。

这意味着生成的代码的优化变得越来越重要。我希望宣布采用LLVM后端将显著加速很多代码,因此纯Delphi代码不再是一个问题。

很抱歉,没有解决方案,但或许有一些解释。

更新

从XE4开始,许多FastCode例程已经取代了我在上面段落谈论的未优化例程。它们通常仍然是PUREPASCAL,但是它们代表了一种很好的优化。因此,情况并不像过去那样糟糕。 TStringHelper和普通字符串例程在OS X中仍然显示出一些错误和一些极其缓慢的代码(特别是涉及从Unicode到Ansi或反之的转换),但是RTL的Win64部分似乎要好得多。


3
基准测试是有好处的,如果在整个项目上进行,并且只有在分析表明某些例程确实需要加速时,优化才有用。Knuth已经警告过不要过早地进行优化。 - Rudy Velthuis
2
也许我们可以在社区中开展一个新的“Fastcode64”项目。 - Warren P
1
我在FastCode邮件列表上看到了一些评论,称有一个临时的64位汇编版本的FastCode Pos函数。我猜他们也会看看其他函数。虽然这些人知道很多我不知道的技巧,但我甚至可能会帮助他们。 - Rudy Velthuis
1
我曾认为“本地CPU”编译器比其他任何技术都要优越,因此即使是从“PUREPASCAL”实现中,它也应该能够创建接近完美性能的代码 ;) - marc hoffman
我刚在XE4 x32模式下使用FastCode purepascal的PosEx_Sha_Pas_2System.Pos进行了比较。令我惊讶的是,在Unicode字符串中,purepascal版本更快。在x64模式下,PosEx_Sha_Pas_2几乎与x32模式下一样快。 - LU RD
显示剩余25条评论

6

尽量避免在循环中进行任何字符串分配。

在您的情况下,可能涉及x64调用约定的堆栈准备。您是否尝试将IsStrANumber声明为inline

我猜这会使它更快。

function IsStrANumber(P: PAnsiChar): Boolean; inline;
begin
  Result := False;
  if P=nil then exit;
  while P^ <> #0 do
    if not (P^ in ['0'..'9']) then 
      Exit else
      Inc(P);
  Result := True;
end;

procedure TForm11.Button1Click(Sender: TObject);
Const x = '1234567890';
Var a,y,z: Integer;
    s: AnsiString;
begin
  z := GetTickCount;
  s := x;
  for a := 1 to 99999999 do begin
   if IsStrANumber(pointer(s)) then y := 0;//StrToInt(x);
  end;
  Caption := IntToStr(GetTickCount-z);
end;

"纯Pascal"版本的RTL确实是这里速度缓慢的原因...

请注意,与32位版本相比,FPC 64位编译器甚至更糟糕...听起来Delphi编译器不是唯一一个问题!64位并不意味着“更快”,无论市场营销怎么说!有时甚至相反(例如,已知JRE在64位上运行较慢,并且在涉及指针大小时将引入新的x32模型)。


内联确实会多花费几毫秒,但在64位系统中仍需要近两倍的时间。当然,64位并不意味着它自动更快,但是当你需要等待10分钟来处理大型文本文件而不是5分钟时,速度慢一半是很重要的。 - hikari
1
你试过我的确切版本了吗?仅仅添加“inline”是不够的。你需要摆脱整个字符串<->ansistring转换等等。使用临时变量spointer(s)应该可以使它更快。 - Arnaud Bouchez
是的,你的代码快了一点,但我示例中使用的常量仅用于展示代码,在我的程序中都是变量。尽管如此,编译为64位仍然极慢,2-3倍。 - hikari

5
代码可以像这样编写,具有良好的性能结果:
function IsStrANumber(const S: AnsiString): Boolean; inline;
var
  P: PAnsiChar;
begin
  Result := False;
  P := PAnsiChar(S);
  while True do
  begin
    case PByte(P)^ of
      0: Break;
      $30..$39: Inc(P);
    else
      Exit;
    end;
  end;
  Result := True;
end;

英特尔(R) Core(TM)2 CPU T5600 @ 1.83GHz

  • x32位:2730毫秒
  • x64位:3260毫秒

英特尔(R) Pentium(R) D CPU 3.40GHz

  • x32位:2979毫秒
  • x64位:1794毫秒

展开循环可以使执行更快:

function IsStrANumber(const S: AnsiString): Boolean; inline; 
type
  TStrData = packed record
    A: Byte;
    B: Byte;
    C: Byte;
    D: Byte;
    E: Byte;
    F: Byte;
    G: Byte;
    H: Byte;
  end;
  PStrData = ^TStrData;
var
  P: PStrData;
begin
  Result := False;
  P := PStrData(PAnsiChar(S));
  while True do
  begin
    case P^.A of
      0: Break;
      $30..$39:
        case P^.B of
          0: Break;
          $30..$39:
            case P^.C of
              0: Break;
              $30..$39:
                case P^.D of
                  0: Break;
                  $30..$39:
                    case P^.E of
                      0: Break;
                      $30..$39:
                        case P^.F of
                          0: Break;
                          $30..$39:
                            case P^.G of
                              0: Break;
                              $30..$39:
                                case P^.H of
                                  0: Break;
                                  $30..$39: Inc(P);
                                else
                                  Exit;
                                end;
                            else
                              Exit;
                            end;
                        else
                          Exit;
                        end;
                    else
                      Exit;
                    end;
                else
                  Exit;
                end;
            else
              Exit;
            end;
        else
          Exit;
        end;
    else
      Exit;
    end;
  end;
  Result := True;
end;

英特尔(R) Core(TM)2 CPU T5600 @ 1.83GHz

  • x32位 : 2199 毫秒
  • x64位 : 1934 毫秒

英特尔(R) Pentium(R) D CPU 3.40GHz

  • x32位 : 1170 毫秒
  • x64位 : 1279 毫秒

如果你也采用了Arnaud Bouchez所说的方法,你可以使它更快。


这个有点奇怪:为了提高速度而删除0的情况(以及之前的nil检查):32位:811 vs 656,64位:1108 vs 1654。因此,在32位中,这个例子比较慢,但在64位中,它更快oO。 - hikari
保持0检查以确保安全性:32位:1607毫秒,64位:1060。 - hikari

2

64位环境下,测试p^ in ['0'..'9']速度较慢。

为了替代in []测试,添加了一个内联函数,用于测试上下边界以及空字符串。

function IsStrANumber(const S: AnsiString): Boolean; inline;
var
  P: PAnsiChar;
begin
  Result := False;
  P := Pointer(S);
  if (P = nil) then
    Exit;
  while P^ <> #0 do begin
    if (P^ < '0') then Exit;
    if (P^ > '9') then Exit;
    Inc(P);
  end;
  Result := True;
end;

基准测试结果:

        x32     x64
--------------------
hikari  1420    3963
LU RD   1029    1060

在32位系统中,主要速度差别在于内联和P := PAnsiChar(S);将调用一个外部RTL例程进行nil检查,然后再分配指针值,而P := Pointer(S);只是分配指针。
观察到这里的目标是测试字符串是否为数字,然后进行转换, 为什么不使用RTLTryStrToInt(),它可以一步完成所有操作,并处理符号、空格等。
通常,在对程序进行性能分析和优化时,最重要的是找到解决问题的正确方法。

感谢您的补充。TryStrToInt方法似乎比其他方法慢了大约8倍。 - hikari
主题发起人只需要检查数字,而StrToInt将转换字符串,这总是会更慢。此外,在大量错误结果的情况下,Try*例程更慢,因为会引发异常。 - Fr0sT
@Fr0sT,Try*例程没有引发异常。这里的命中/失误因素是确定调用IsStrANumber加IntToStr的最佳结果还是仅调用TryStrToInt。如果所有内容都是数字,则后者大约快两倍,在最坏情况下慢20%。目标是否只是检查字符串是否为数字或实际将字符串转换为数字有点不清楚。无论如何,所提出的IsStrANumber版本似乎满足OP的要求。 - LU RD
是的,没错,我被“Try”前缀搞糊涂了。 - Fr0sT

1
64位的好处在于地址空间,而不是速度(除非您的代码受可寻址内存的限制)。
历史上,在更宽的机器上进行字符操作代码始终比较慢。从16位的8088/8086移动到32位的386也是如此。将8位字符放入64位寄存器中会浪费内存带宽和缓存。
为了提高速度,您可以避免使用char变量,使用指针,使用查找表,使用位并行性(在一个64位字中操作8个字符),或使用SSE/SSE2...指令。显然,其中一些将使您的代码依赖于CPUID。此外,在调试时打开CPU窗口,并查看编译器是否在“为”您执行愚蠢的操作,例如静默字符串转换(特别是在调用周围)。
您可以尝试查看FastCode库中的一些本地Pascal例程。例如,PosEx_Sha_Pas_2虽然不如汇编版本快,但比RTL代码快(在32位下)。

PosEx_Sha_Pas_2 在64位系统中实际上比 Pos 慢了约60-70%(在32位系统中慢了10倍)。 - hikari

1

这里有两个函数。一个仅检查正数,第二个同时检查负数。并且没有大小限制。第二个比常规的Val快4倍。

function IsInteger1(const S: String): Boolean; overload;
var
  E: Integer;
  Value: Integer;
begin
  Val(S, Value, E);
  Result := E = 0;
end;


function IsInteger2(const S: String): Boolean; inline; 
var
    I: Integer;
begin
    Result := False;
    I := 0;
  while True do
  begin
    case Ord(S[I+1]) of
      0: Break;
      $30..$39:
        case Ord(S[I+2]) of
          0: Break;
          $30..$39:
            case Ord(S[I+3]) of
              0: Break;
              $30..$39:
                case Ord(S[I+4]) of
                  0: Break;
                  $30..$39:
                    case Ord(S[I+5]) of
                      0: Break;
                      $30..$39:
                        case Ord(S[I+6]) of
                          0: Break;
                          $30..$39:
                            case Ord(S[I+7]) of
                              0: Break;
                              $30..$39:
                                case Ord(S[I+8]) of
                                  0: Break;
                                  $30..$39:
                                    case Ord(S[I+9]) of
                                      0: Break;
                                      $30..$39: 
                                        case Ord(S[I+10]) of
                                          0: Break;
                                          $30..$39: Inc(I, 10);
                                        else
                                          Exit;
                                        end;
                                    else
                                      Exit;
                                    end;
                                else
                                  Exit;
                                end;
                            else
                              Exit;
                            end;
                        else
                          Exit;
                        end;
                    else
                      Exit;
                    end;
                else
                  Exit;
                end;
            else
              Exit;
            end;
        else
          Exit;
        end;
    else
      Exit;
    end;
  end;
  Result := True;
end;

function IsInteger3(const S: String): Boolean; inline;
var
  I: Integer;
begin
  Result := False;
  case Ord(S[1]) of
    $2D,
    $30 .. $39:
    begin
      I := 1;
      while True do
      case Ord(S[I + 1]) of
        0:
        Break;
        $30 .. $39:
        case Ord(S[I + 2]) of
          0:
          Break;
          $30 .. $39:
          case Ord(S[I + 3]) of
            0:
            Break;
            $30 .. $39:
            case Ord(S[I + 4]) of
              0:
              Break;
              $30 .. $39:
              case Ord(S[I + 5]) of
                0:
                Break;
                $30 .. $39:
                case Ord(S[I + 6]) of
                  0:
                  Break;
                  $30 .. $39:
                  case Ord(S[I + 7]) of
                    0:
                    Break;
                    $30 .. $39:
                    case Ord(S[I + 8]) of
                      0:
                      Break;
                      $30 .. $39:
                      case Ord(S[I + 9]) of
                        0:
                        Break;
                        $30 .. $39:
                        case Ord(S[I + 10]) of
                          0:
                          Break;
                          $30 .. $39:
                          case Ord(S[I + 11]) of
                            0:
                            Break;
                            $30 .. $39:
                            case Ord(S[I + 12]) of
                              0:
                              Break;
                              $30 .. $39:
                              case Ord(S[I + 13]) of
                                0:
                                Break;
                                $30 .. $39:
                                Inc(I, 13);
                              else
                                Exit;
                              end; 
                            else
                              Exit;
                            end; 
                          else
                            Exit;
                          end; 
                        else
                          Exit;
                        end; 
                      else
                        Exit;
                      end; 
                    else
                      Exit;
                    end; 
                  else
                    Exit;
                  end; 
                else
                  Exit;
                end; 
              else
                Exit;
              end;  
            else
              Exit;
            end;  
          else
            Exit;
          end;   
        else
          Exit;
        end;    
      else
        Exit;
      end;
    end;
  else
    Exit;
  end;
  Result := True;
end;

这似乎是目前最快的方法,尽管如果您使用AnsiString调用它,它会冻结。谢谢。 - hikari
@hikari 尝试使用 S: String 和这个函数调用 IsStrANumberIsStrANumber 中有一个字符串转换。 - user3323367

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