4

Windows 10でDelphi 10.1 Berlinを使用しています。

サイズの異なるレコードが 2 つあります。TList<T>これらの 2 つのレコードをループして経過時間をテストするコードを作成しました。より大きなレコードのリストをループすると、実行速度が大幅に低下します。

誰でも理由を説明し、ループをより速く実行するための解決策を提供できますか?

type
  tTestRecord1 = record
    Field1: array[0..4] of Integer;
    Field2: array[0..4] of Extended;
    Field3: string;
  end;

  tTestRecord2 = record
    Field1: array[0..4999] of Integer;
    Field2: array[0..4999] of Extended;
    Field3: string;
  end;

procedure TForm1.Button1Click(Sender: TObject);
var
  _List: TList<tTestRecord1>;
  _Record: tTestRecord1;
  _Time: TTime;
  i: Integer;
begin
  _List := TList<tTestRecord1>.Create;

  for i := 0 to 4999 do
  begin
    _List.Add(_Record);
  end;

  _Time := Time;

  for i := 0 to 4999 do
  begin
    if _List[i].Field3 = 'abcde' then
    begin
      Break;
    end;
  end;

  Button1.Caption := FormatDateTime('s.zzz', Time - _Time); // 0.000

  _List.Free;
end;

procedure TForm1.Button2Click(Sender: TObject);
var
  _List: TList<tTestRecord2>;
  _Record: tTestRecord2;
  _Time: TTime;
  i: Integer;
begin
  _List := TList<tTestRecord2>.Create;

  for i := 0 to 4999 do
  begin
    _List.Add(_Record);
  end;

  _Time := Time;

  for i := 0 to 4999 do
  begin
    if _List[i].Field3 = 'abcde' then
    begin
      Break;
    end;
  end;

  Button2.Caption := FormatDateTime('s.zzz', Time - _Time); // 0.045

  _List.Free;
end;
4

1 に答える 1

9

まず第一に、コード全体を検討したいと思います。あなたが時間を計っていないことに気付いたリストにデータを入力するコードも含めて。2 番目のレコードはサイズが大きいため、そのレコード タイプの割り当てを行う場合は、より多くのメモリをコピーする必要があります。さらに、リストから読み取ると、大きなレコードは小さなレコードよりもキャッシュに優しくなく、パフォーマンスに影響します。この後者の影響は、前者ほど重要ではありません。

これに関連して、アイテムを追加すると、リストの内部レコード配列のサイズを変更する必要があります。サイズ変更により、インプレースで実行できない再割り当てが発生する場合があります。それが発生すると、メモリの新しいブロックが割り当てられ、以前のコンテンツがこの新しいブロックにコピーされます。そのコピーは、より大きなレコードに対して明らかに高価です。配列の長さがわかっている場合は、配列を一度割り当てることでこれを軽減できます。リストCapacityは使用するメカニズムです。もちろん、常に事前に長さがわかるわけではありません。

あなたのプログラムは、メモリ割り当てとメモリ アクセス以外にはほとんど何もしません。したがって、これらのメモリ操作のパフォーマンスが支配的です。

ここで、タイミングはリストから読み取るコードのみです。したがって、人口のメモリ コピー パフォーマンスの違いは、実行したベンチマークの一部ではありません。以下で説明するように、タイミングの違いは主に、読み取り時の過度のメモリコピーにあります。

次のコードを検討してください。

if _List[i].Field3 = 'abcde' then

はレコードであり、値型であるため_List[i]、レコード全体が暗黙的な隠しローカル変数にコピーされます。コードは実際には次と同等です。

var
  tmp: tTestRecord2;
...
tmp := _List[i]; // copy of entire record
if tmp.Field3 = 'abcde' then

このコピーを回避するには、いくつかの方法があります。

  1. 基になる型を参照型に変更します。これにより、メモリ管理要件が変更されます。そして、値型を使用したい正当な理由があるかもしれません。
  2. アイテムのコピーではなく、アイテムのアドレスを返すことができるコンテナー クラスを使用します。
  3. からTList<T>動的配列に切り替えますTArray<T>。この単純な変更により、コンパイラは、レコード全体をコピーすることなく、個々のフィールドに直接アクセスできるようになります。
  4. を使用しTList<T>.Listて、データを保持するリスト オブジェクトの基になる配列へのアクセスを取得します。これは、前の項目と同じ効果があります。

上記の項目 4 は、大きな違いを確認するための最も簡単な変更です。あなたは交換します

if _List[i].Field3 = 'abcde' then

if _List.List[i].Field3 = 'abcde' then

これにより、パフォーマンスに非常に大きな変化が生じるはずです。

このプログラムを考えてみましょう:

{$APPTYPE CONSOLE}

uses
  System.Diagnostics,
  System.Generics.Collections;

type
  tTestRecord2 = record
    Field1: array[0..4999] of Integer;
    Field2: array[0..4999] of Extended;
    Field3: string;
  end;

procedure Main;
const
  N = 100000;
var
  i: Integer;
  Stopwatch: TStopwatch;
  List: TList<tTestRecord2>;
  Rec: tTestRecord2;
begin
  List := TList<tTestRecord2>.Create;
  List.Capacity := N;

  for i := 0 to N-1 do
  begin
    List.Add(Rec);
  end;

  Stopwatch := TStopwatch.StartNew;
  for i := 0 to N-1 do
  begin
    if List[i].Field3 = 'abcde' then
    begin
      Break;
    end;
  end;
  Writeln(Stopwatch.ElapsedMilliseconds);
end;

begin
  Main;
  Readln;
end.

メモリ不足の状態を避けるために、64 ビット用にコンパイルする必要がありました。私のマシンの出力は約 700 です。 に変更List[i].Field3するList.List[i].Field3と、出力は 1 桁になります。タイミングはかなり大雑把ですが、これは要点を示していると思います。


大きなレコードがキャッシュに適していないという問題は残ります。これは対処がより複雑であり、実際のコードがそのデータに対してどのように動作したかを詳細に分析する必要があります。


余談ですが、パフォーマンスを重視する場合は使用しませんExtended。サイズは 2 のべき乗ではなく 10 であるため、メモリ アクセスのアライメントが頻繁にずれます。のエイリアスであるDoubleorを使用します。RealDouble

于 2016-08-17T07:42:18.460 に答える