1

私はいくつかの強化された文字列関連の関数をテストしています。これを使用して、ポインターを掘り下げることなく、より高速で効率的に使用するために文字列をコピーする方法として move を使用しようとしています。

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.

解決策を考案し、可能であれば説明をお願いします。代替案も大歓迎です。

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 を 1 ではなく 0 に初期化します。

ゼロから始まるバイト インデックスを使用してインデックスを作成する必要がある場合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;

Temp1とよりも良い名前をお勧めします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型指定されていないポインターとバイトのカウンターで動作します。文字列 (それぞれ Pascal 文字列と C 文字列) と文字のカウンターを操作しますSystem.CopySysUtils.StrLCopyただし、char と byte は異なる型であるため、string/char コンテキストから pointers/bytes コンテキストに移動する場合は、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 パワーの 1 パーセントを圧迫するという最初の主張は、元のコードには当てはまりません。高速なポインター操作の幻想は、パフォーマンスをまったく気にしない次善のコードによって影が薄くなります。


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

TStringListクラスです。クラス インスタンスの作成と削除は、コストのかかる (時間がかかる) 操作です。Delphi はこれらのクラスの柔軟なフレームワークを作成しましたが、速度は低下します。したがって、信頼性と柔軟性を犠牲にして速度を数パーセント上げたい場合は、クラスを使用しないでください。

DelimiterSize : Byte;

NativeInt代わりに、残りの数値変数と同じようにする必要があります。数バイト節約できたと思いますが、CPU にネイティブでないデータ型を使用させ、時々型キャストを挿入させました。これは、明示的に導入された遅延に他なりません。皮肉なことに、これらのバイトを保存していませんでした。Delphi は 32 ビット境界で次の変数を割り当てるために 3 バイトを追加するだけだったからです。これは、典型的な「メモリ アラインメント」の最適化です。

Result        := ' ';

この値は使用されません。だから、ただの時間のロスです。

for Str in AStringList do

この構築ではTInterfacedObject、仮想メソッドをインスタンス化して呼び出してから、グローバル ロックで参照カウントする必要があり、コストがかかる (遅い) 操作です。また、マルチスレッドのタスクロードでは 2 倍遅くなります。数パーセントの速度を絞る必要がある場合は、for-in ループで数十パーセントを失うことを避ける必要があります。これらの高レベルのループは、便利で信頼性が高く、柔軟性がありますが、その代償としてスピードがあります。

 for Str in AStringList do

その後、あなたはそれを2回行います。しかし、その文字列リストがどのように実装されているかはわかりません。どのくらい効率的に文字列を取得しますか? TMemo.Lines のように、別のプロセスにメッセージを渡すことさえあります。そのため、そのクラスとその多数の内部仮想メンバーへのすべてのアクセスを最小限に抑える必要があります。すべての文字列をローカル変数に 1 回キャッシュします。それらすべてを 2 回フェッチしないでください。

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

ここで、非常に興味深い質問にたどり着きました。ポインターとバイトを使用することで、速度の利点を得ることができる仮想的な場所はありますか? CPU ウィンドウを開いて、その行が実際にどのように実装されているかを見てください!

文字列は参照カウントされます! これを行うとStr2 := Str1;、データはコピーされませんが、ポインターのみがコピーされます。しかし、文字列(そのStr[1]式)内の実メモリ バッファへのアクセスを開始すると、コンパイラはそれ以上参照をカウントできないため、Delphi はここで参照カウンタを 1 つの SINGLE に減らすことを余儀なくされます。つまり、Delphi はここで をUniqueString何度も呼び出さなければStrなりResultません。System.UniqueStringrefcounter をチェックし、それが 1 より大きい場合は、文字列の特別なローカル コピーを作成します (すべてのデータを新しく割り当てられた特別なバッファにコピーします) 。次に、MoveDelphi RTL が行うのと同じように、次のことを行います。速度の利点がどこから来るのかわかりませんか?

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

そして、ここでも同じ操作が繰り返されます。そして、それらはコストのかかる操作です。少なくとも余分な手順が呼び出され、最悪の場合、新しいバッファが割り当てられ、すべてのコンテンツがコピーされます。


履歴書:

  1. 参照カウント文字列と生のポインタとの間の境界はコストのかかるものであり、境界を越えるたびに、Delphi に代償を払わせます。

  2. これらの境界を同じコードに混在させると、代償が何度も何度も支払われます。また、カウンターとインデックスがバイトを参照する場所と文字を参照する場所を混乱させます。

  3. Delphi は何年もの間、カジュアルな文字列操作を最適化しました。そして、そこでかなり良い仕事をしました。Delphi を凌駕することは可能ですが、プログラムの Pascal ソースの背後にあるものを、各 CPU アセンブラ命令まで、非常に詳細に理解する必要があります。それは汚くて退屈な作業です。信頼性が高く柔軟なものを for-in ループや TStrings クラスとして使用するという贅沢はありません。

  4. 最終的には、ほとんどの場合、速度が数パーセント向上しますが、誰も気付かないでしょう。しかし、その代償として、理解、記述、読み取り、およびテストがはるかに困難なコードを使用することになります。これらの数パーセントの速度は、保守不可能なコードに値するでしょうか? 疑わしい。

したがって、強制されない限り、私のアドバイスは、時間を無駄にするのをスキップして、通常の操作を行うことです。Str := JclStringList().Add(['Hello1','Hello2','Hello3','Hello4']).Join(';'); 信頼性と柔軟性は、ほとんどの場合、速度よりも優先されます。

申し訳ありませんが、速度の最適化についてはよくわかりませんが、Delphi 自体よりも高速にしようとしているコードで、速度を損なう問題を簡単に見つけました。私の経験は、文字列分野で Delphi を凌駕しようとすることさえ、何マイルも離れています。そして、他の可能性はないと思いますが、最終的に在庫のものよりもパフォーマンスが低下するために多くの時間を無駄にしています.

于 2013-11-06T09:49:44.100 に答える