1

我正在测试一些增强的字符串相关函数,我试图使用 move 作为一种复制字符串的方法,以便更快、更有效地使用,而无需深入研究指针。

在测试从 TStringList 生成分隔字符串的函数时,我遇到了一个奇怪的问题。编译器在索引为空时引用通过索引包含的字节,并且当通过移动将字符串添加到其中时,索引引用包含的字符。

这是一个小型缩小的准系统代码示例:-

unit UI;

interface

uses
  System.SysUtils, System.Types, System.UITypes, System.Rtti, System.Classes,
  System.Variants, FMX.Types, FMX.Controls, FMX.Forms, FMX.Dialogs, FMX.Layouts,
  FMX.Memo;

type
  TForm1 = class(TForm)
    Results: TMemo;
    procedure FormCreate(Sender: TObject);
  end;

var
  Form1: TForm1;

implementation

{$R *.fmx}

function  StringListToDelimitedString
          ( const AStringList: TStringList; const ADelimiter: String ): String;
var
  Str           : String;
  Temp1         : NativeInt;
  Temp2         : NativeInt;
  DelimiterSize : Byte;

begin

  Result        := ' ';
  Temp1         := 0;
  DelimiterSize := Length ( ADelimiter ) * 2;

  for Str in AStringList do
    Temp1 := Temp1 + Length ( Str );

  SetLength ( Result, Temp1 );
  Temp1     := 1;

  for Str in AStringList do
  begin

    Temp2 := Length ( Str ) * 2;

    // Here Index references bytes in Result
    Move  ( Str [1],        Result [Temp1], Temp2 );

    // From here the index seems to address characters instead of bytes in Result
    Temp1 := Temp1 + Temp2;
    Move  ( ADelimiter [1], Result [Temp1], DelimiterSize );    
    Temp1 := Temp1 + DelimiterSize;

  end;

end;

procedure TForm1.FormCreate(Sender: TObject);
var
  StrList : TStringList;
  Str     : String;

begin

  // Test 1 : StringListToDelimitedString

  StrList := TStringList.Create;
  Str     := '';

  StrList.Add ( 'Hello1' );
  StrList.Add ( 'Hello2' );
  StrList.Add ( 'Hello3' );
  StrList.Add ( 'Hello4' );

  Str := StringListToDelimitedString ( StrList, ';' );
  Results.Lines.Add ( Str ); 
  StrList.Free;

end;

end.

请设计一个解决方案,如果可能的话,一些解释。也欢迎替代品。

4

2 回答 2

6

让我们看一下关键的代码:

// Here Index references bytes in Result
Move  ( Str [1],        Result [Temp1], Temp2 );

// From here the index seems to address characters instead of bytes in Result
Temp1 := Temp1 + Temp2;
Move  ( ADelimiter [1], Result [Temp1], DelimiterSize );    

现在,一些解释。当你索引一个字符串时,你总是在索引字符。您永远不会索引字节。在我看来,您好像希望索引字节。在这种情况下,使用字符串索引运算符会使生活变得艰难。所以我建议你按如下方式索引字节。

首先将 Temp1 初始化为 0 而不是 1,因为我们将使用从零开始的索引。

当您需要Result使用从零开始的字节索引进行索引时,请执行以下操作:

PByte(Result)[Temp1]

所以你的代码变成:

Temp1 := 0;
for Str in AStringList do
begin
  Temp2 := Length(Str)*2;
  Move(Str[1], PByte(Result)[Temp1], Temp2);
  Temp1 := Temp1 + Temp2;
  Move(ADelimiter[1], PByte(Result)[Temp1], DelimiterSize);    
  Temp1 := Temp1 + DelimiterSize;
end;

事实上,我想我会这样写,避免所有字符串索引:

Temp1 := 0;
for Str in AStringList do
begin
  Temp2 := Length(Str)*2;
  Move(Pointer(Str)^, PByte(Result)[Temp1], Temp2);
  Temp1 := Temp1 + Temp2;
  Move(Pointer(ADelimiter)^, PByte(Result)[Temp1], DelimiterSize);    
  Temp1 := Temp1 + DelimiterSize;
end;

我建议比Temp1and更好的名字Temp2。我也质疑NativeInt这里的使用。我通常希望看到Integer. 尤其是因为 Delphistring由带符号的 32 位值索引。长度不能string大于 2GB。

另请注意,您没有分配足够的内存。您忘记考虑分隔符的长度。修复它,你的函数看起来像这样:

function StringListToDelimitedString(const AStringList: TStringList;
  const ADelimiter: String): String;
var
  Str: String;
  Temp1: Integer;
  Temp2: Integer;
  DelimiterSize: Integer;
begin
  Temp1 := 0;
  DelimiterSize := Length(ADelimiter) * SizeOf(Char);

  for Str in AStringList do
    inc(Temp1, Length(Str) + DelimiterSize);

  SetLength(Result, Temp1);
  Temp1 := 0;
  for Str in AStringList do
  begin
    Temp2 := Length(Str) * SizeOf(Char);
    Move(Pointer(Str)^, PByte(Result)[Temp1], Temp2);
    inc(Temp1, Temp2);
    Move(Pointer(ADelimiter)^, PByte(Result)[Temp1], DelimiterSize);
    inc(Temp1, DelimiterSize);
  end;
end;

如果你想避免指针,那么这样写:

function StringListToDelimitedString(const AStringList: TStringList;
  const ADelimiter: String): String;
var
  Str: String;
  StrLen: Integer;
  ResultLen: Integer;
  DelimiterLen: Integer;
  ResultIndex: Integer;
begin
  DelimiterLen := Length(ADelimiter);

  ResultLen := 0;
  for Str in AStringList do
    inc(ResultLen, Length(Str) + DelimiterLen);

  SetLength(Result, ResultLen);

  ResultIndex := 1;
  for Str in AStringList do
  begin
    StrLen := Length(Str);
    Move(Pointer(Str)^, Result[ResultIndex], StrLen*SizeOf(Char));
    inc(ResultIndex, StrLen);
    Move(Pointer(ADelimiter)^, Result[ResultIndex], DelimiterLen*SizeOf(Char));
    inc(ResultIndex, DelimiterLen);
  end;
end;
于 2013-11-06T09:20:27.067 回答
3

System.Move适用于无类型指针和字节计数器。System.CopySysUtils.StrLCopy使用字符串(分别为 Pascal 字符串和 C 字符串)和字符计数器。但是 char 和 byte 是不同的类型,因此当您从字符串/字符上下文移动到指针/字节上下文时 - 您应该重新计算以字节为单位的长度。顺便说一句,索引也是如此,Result [Temp1]以字符而不是字节计算。并且一直如此。

正确的解决方案不是混合不同星球的公民。如果你想要指针 - 使用指针。如果你想要字符和字符串 - 使用字符和字符串。但不要混合它们!分而治之,始终分开并明确何时使用原始指针以及在何处使用键入的字符串!否则你是在误导自己;

function  StringListToDelimitedString
          ( const AStringList: TStrings; const ADelimiter: String ): String;
var
  Str           : array of String;
  Lengths       : array of Integer;
  Temp1         : NativeInt;
  Count, TotalChars : Integer;

  PtrDestination: PByte;
  PCurStr: ^String;
  CurLen: Integer;

  Procedure  Add1(const Source: string);
  var count: integer; // all context is in bytes, not chars here!
      Ptr1, Ptr2: PByte; 
  begin
    if Source = '' then exit;
    Ptr1 := @Source[ 1 ];
    Ptr2 := @Source[ Length(Source)+1 ];
    count := ptr2 - ptr1;

    Move( Source[1], PtrDestination^, count);
    Inc(PtrDestination, count);
  end;

begin // here all context is in chars and typed strings, not bytes
  Count := AStringList.Count;
  if Count <= 0 then exit('');

  SetLength(Str, Count); SetLength(Lengths, Count);
  TotalChars := 0; 
  for Temp1 := 0 to Count - 1 do begin
      PCurStr  := @Str[ Temp1 ]; 
      PCurStr^ := AStringList[ Temp1 ]; // caching content, avoiding extra .Get(I) calls
      CurLen := Length ( PCurStr^ ); // caching length, avoind extra function calls
      Lengths[ Temp1 ] := CurLen;
      Inc(TotalChars,  CurLen);
  end;

  SetLength ( Result, TotalChars + ( Count-1 )*Length( ADelimiter ) );

  PtrDestination := Pointer(Result[1]); 
  // Calls UniqueString to get a safe pointer - but only once 

  for Temp1 := Low(Str) to High(Str) do
  begin
    Add1( Str[ Temp1 ] );
    Dec( Count );
    if Count > 0 // not last string yet
       then Add1( Delimeter );
  end;
end;

现在,我认为正确的解决方案是停止发明自行车并使用现成的和经过测试的库,例如。

 Str := JclStringList().Add(['Hello1','Hello2','Hello3','Hello4']).Join(';');

或者,如果您确实需要添加分隔符 PAST THE LAST 字符串(通常会小心避免),那么

 Str := JclStringList().Add(['Hello1','Hello2','Hello3','Hello4', '']).Join(';');

压缩单个百分比的 CPU 功率的原始声明与原始代码不符。快速指针操作的错觉只是被完全不关心性能的次优代码所掩盖。


function  StringListToDelimitedString
          ( const AStringList: TStringList; const ADelimiter: String ): String;

TStringList是一类。类实例的创建和删除是昂贵(缓慢)的操作。Delphi 为这些类制作了一个灵活的框架——但速度受到影响。因此,如果您想获得一些额外的速度百分比并以牺牲可靠性和灵活性为代价 - 不要使用类。

DelimiterSize : Byte;

相反,它应该与NativeInt那里的其他穆勒变量一样。您认为您只是节省了几个字节 - 但您强迫 CPU 使用非本机数据类型并不时插入类型转换。这只不过是一个明确引入的延迟。具有讽刺意味的是,您甚至没有保存这些字节,因为 Delphi 只会多填充三个字节以在 32 位边界上分配下一个变量。那是典型的“内存对齐”优化。

Result        := ' ';

这个值永远不会被使用。所以这只是浪费时间。

for Str in AStringList do

这种结构需要实例化TInterfacedObject并调用其虚拟方法,然后使用全局锁定对其进行引用计数 - 是昂贵(缓慢)的操作。多线程任务加载速度慢了两倍。如果您需要压缩几个百分比的速度 - 您应该避免在 for-in 循环中丢失几十个百分比。这些高级循环方便、可靠且灵活——但他们为此付出了速度。

 for Str in AStringList do

然后你做两次。但是您不知道 stringlist 是如何实现的。它获取字符串的效率如何?它甚至可以像 TMemo.Lines 那样将消息传递给另一个进程!因此,您应该尽量减少对该类及其众多内部虚拟成员的所有访问。将所有字符串一次缓存在某个局部变量中,不要每次都获取两次!

 Move  ( Str [1],        Result [Temp1], Temp2 );

现在我们遇到了一个非常有趣的问题——是否有假设的地方可以通过使用指针和字节来获得任何速度优势?打开 CPU 窗口,看看这条线是如何实现的!

字符串是引用计数的!当您这样做时,Str2 := Str1;不会复制任何数据,而只会复制指针。但是当你开始访问字符串中的真实内存缓冲区时——那个Str[1]表达式——编译器不能更多地计算引用,所以 Delphi 在这里被迫将引用计数器减少到一个。也就是说,Delphi在这里被迫UniqueString一遍Str又一遍地调用Result;检查System.UniqueStringrefcounter,如果它大于 1,则制作字符串的特殊本地副本(将所有数据复制到新分配的特殊缓冲区中)。然后你做一个Move- 就像 Delphi RTL 做的一样。我无法得到速度的任何优势可能来自哪里?

 Move  ( ADelimiter [1], Result [Temp1], DelimiterSize )

在这里再次进行相同的操作。它们是昂贵的操作!至少调用了一个额外的过程,最坏的情况是分配新缓冲区并复制所有内容。


恢复:

  1. 引用计数字符串和原始指针之间的界限是一个代价高昂的界限,每次越过它 - 就迫使 Delphi 付出代价。

  2. 在同一个代码中混合这些边界会一次又一次地付出代价。它还会让您自己感到困惑,您的计数器和索引在哪里引用字节以及它们在哪里引用字符。

  3. Delphi 多年来优化了临时字符串操作。并在那里做得很好。超越 Delphi 是可能的——但您需要非常详细地了解每个 CPU 汇编程序指令——程序中 Pascal 源代码背后的内容。这是一项肮脏而乏味的工作。使用这些可靠和灵活的东西作为 for-in 循环和 TStrings 类将不会有那么奢侈。

  4. 最后,您很可能会获得百分之几的速度增益,这是没人会注意到的。但是你会用更难理解、编写、阅读和测试的代码来为此付出代价。那些百分之几的速度值得无法维护的代码吗?我对此表示怀疑。

因此,除非您被迫这样做,否则我的建议是不要浪费您的时间,而只是做通常的事情Str := JclStringList().Add(['Hello1','Hello2','Hello3','Hello4']).Join(';'); 可靠性和灵活性几乎总是比纯粹的速度更可取。

很遗憾地告诉你,虽然我对速度优化知之甚少,但我很容易在你的代码中看到一个破坏速度的问题,你打算比 Delphi 本身更快。我的经验是千里之外,甚至试图在弦乐领域超越德尔福。而且我认为您没有任何机会,除了浪费大量时间最终获得比股票更差的性能。

于 2013-11-06T09:49:44.100 回答