用字符串移动的奇怪行为

时间:2013-11-06 09:09:06

标签: delphi firemonkey-fm2

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

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

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

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.

请设计解决方案,如有可能,请提供一些解释。也欢迎替代品。

2 个答案:

答案 0 :(得分:5)

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

// 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;

我建议使用比Temp1Temp2更好的名字。我也在这里质疑NativeInt的用法。我通常希望看到Integer。尤其是因为Delphi string被带符号的32位值索引。您不能拥有长度大于2GB的string

另请注意,您没有分配足够的内存。你忘了考虑分隔符的长度。修复此问题,您的功能如下所示:

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;

答案 1 :(得分:3)

System.Move适用于无类型指针和字节计数器。 System.CopySysUtils.StrLCopy使用字符串(分别为Pascal字符串和C字符串)和字符计数器。但是char和byte是不同的类型,所以当你从string / char上下文移动到指针/字节上下文时 - 你应该以字节为单位重新计算字符长度。顺便说一句,关于索引,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位边界上分配下一个变量。这是典型的&#34;内存对齐&#34;优化

Result        := ' ';

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

for Str in AStringList do

这种构造需要实例化TInterfacedObject并调用其虚拟方法然后使用全局锁定引用计数 - 这是一种昂贵(缓慢)的操作。在多线程任务负载中两次变慢。如果你需要挤出几个百分之一的速度 - 你应该避免在for-in循环中减少几十个百分点。那些高级循环是方便,可靠和灵活的 - 但他们为此付出了速度。

 for Str in AStringList do

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

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

现在我们来一个非常有趣的问题 - 是否有一个假想的地方可以通过指针和字节获得任何速度优势?打开CPU窗口,看看该行是如何实现的!

字符串是引用计数的!执行Str2 := Str1;时,不会复制任何数据,只会复制指针。但是当你开始访问字符串中的实际内存缓冲区时 - Str[1]表达式 - 编译器无法计算引用数,因此Delphi强制将参考coutner减少到ONE SINGLE。也就是说,德尔福被迫在UniqueStringStr之上致电Result; System.UniqueString检查refcounter,如果是> 1,则生成字符串的特殊本地副本(将所有数据复制到新分配的特殊缓冲区中)。然后你做一个Move - 就像Delphi RTL自己做的那样。我无法获得速度的任何优势?

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

这里再次进行相同的操作。而且它们的运营成本很高!至少调用一个额外的过程,最坏的情况是分配新缓冲区并复制所有内容。


恢复:

  1. 引用计数字符串和原始指针之间的界限是昂贵的,每次越过它时 - 都会强制Delphi付出代价。

  2. 在相同的代码中混合这些边界会使价格一次又一次地支付。它还会让你自己混淆你的计数器和索引引用字节以及它们引用字符的位置。

  3. Delphi优化了休闲弦乐作业多年。那里做得很好。优秀的Delphi是可能的 - 但你需要了解非常精细的细节 - 直到每个CPU汇编程序指令 - 在你的程序中幕后的Pascal源代码。这是一项肮脏而乏味的工作。对于使用那些可靠和灵活的东西,如for-in循环和TStrings类,没有任何奢侈品可以使用。

  4. 最终你很可能会获得几个百分点的速度增益,这是没有人会注意到的。但是你会用更难以理解,编写,阅读和测试的代码来支付费用。这几个速度的百分比是否值得不可维护的代码?我对此表示怀疑。

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

    很遗憾地告诉我,虽然我对速度优化知之甚少,但我很容易在代码中看到一个速度有害的问题,你打算比Delphi本身更快。 我的经验是在数英里之外甚至试图在字符串领域超越Delphi。而且我认为你没有其他任何机会,但浪费了大量时间来最终使性能比库存差。