Delphi 循环浏览大型记录列表时的长延迟

Delphi 循环浏览大型记录列表时的长延迟,delphi,record,tlist,delphi-10.1-berlin,Delphi,Record,Tlist,Delphi 10.1 Berlin,我在Windows10中使用Delphi10.1 我有两张不同大小的唱片。我编写了代码,循环遍历这些记录中的两个TList,以测试运行时间。在较大的记录列表中循环运行要慢得多 有人能解释一下原因,并提供一个解决方案使循环运行得更快吗 type tTestRecord1 = record Field1: array[0..4] of Integer; Field2: array[0..4] of Extended; Field3: string; end; t

我在Windows10中使用Delphi10.1

我有两张不同大小的唱片。我编写了代码,循环遍历这些记录中的两个
TList
,以测试运行时间。在较大的记录列表中循环运行要慢得多

有人能解释一下原因,并提供一个解决方案使循环运行得更快吗

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;
类型
tTestRecord1=记录
字段1:整数的数组[0..4];
字段2:扩展的数组[0..4];
字段3:字符串;
结束;
tTestRecord2=记录
字段1:整数的数组[0..4999];
字段2:扩展的数组[0..4999];
字段3:字符串;
结束;
程序TForm1.按钮1单击(发送方:TObject);
变量
_名单:TList ;;
_记录:tEStrecord1;
_时间:TTime;
i:整数;
开始
_列表:=TList.Create;
对于i:=0到4999 do
开始
_列表。添加(_记录);
结束;
_时间:=时间;
对于i:=0到4999 do
开始
如果_List[i].Field3='abcde',则
开始
打破
结束;
结束;
按钮1.标题:=FormatDateTime('s.zzz',Time-_Time);//0
_列表。免费;
结束;
程序TForm1.按钮2单击(发送方:TObject);
变量
_名单:TList ;;
_记录:tEStrecord2;
_时间:TTime;
i:整数;
开始
_列表:=TList.Create;
对于i:=0到4999 do
开始
_列表。添加(_记录);
结束;
_时间:=时间;
对于i:=0到4999 do
开始
如果_List[i].Field3='abcde',则
开始
打破
结束;
结束;
按钮2.标题:=FormatDateTime('s.zzz',Time-_Time);//0.045
_列表。免费;
结束;

首先,我想考虑整个代码,甚至是填充列表的代码,这些代码是我意识到的,你没有计时。因为第二条记录的大小更大,所以在分配该记录类型时,需要复制更多的内存。此外,当您从列表中读取时,较大的记录比较小的记录更不利于缓存,这会影响性能。后者的影响可能不如前者显著

与此相关的是,当您添加项时,列表的内部记录数组必须调整大小。有时,调整大小会导致无法就地执行的重新分配。发生这种情况时,将分配一个新的内存块,并将以前的内容复制到此新块。对于更大的记录来说,那份拷贝显然更昂贵。如果知道数组的长度,可以通过预先分配一次数组来缓解这种情况。列表
容量
是使用的机制。当然,你并不总是能提前知道时间的长短

除了内存分配和内存访问之外,您的程序做的很少。因此,这些内存操作的性能占主导地位

现在,您的计时仅限于从列表中读取的代码。因此,总体上的内存拷贝性能差异不是您执行的基准测试的一部分。正如我将在下面解释的那样,您的时间差异主要是由于阅读时内存拷贝过多

考虑以下代码:

if _List[i].Field3 = 'abcde' then
因为
\u List[i]
是一个记录,一个值类型,所以整个记录被复制到一个隐式隐藏的局部变量。该代码实际上相当于:

var
  tmp: tTestRecord2;
...
tmp := _List[i]; // copy of entire record
if tmp.Field3 = 'abcde' then
有几种方法可以避免此副本:

  • 将基础类型更改为引用类型。这会改变内存管理要求。您可能有充分的理由想要使用值类型
  • 使用可以返回项目地址而不是项目副本的容器类
  • TList
    切换到动态阵列
    TArray
    。这个简单的更改将允许编译器直接访问单个字段,而无需复制整个记录
  • 使用
    TList.List
    可以访问包含数据的列表对象的底层数组。这将产生与上一项相同的效果
  • 上面的第4项是最简单的改变,你可以看到一个巨大的差异。你会取代

    if _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.
    
    {$APPTYPE控制台}
    使用
    系统诊断,
    系统、泛型、集合;
    类型
    tTestRecord2=记录
    字段1:整数的数组[0..4999];
    字段2:扩展的数组[0..4999];
    字段3:字符串;
    结束;
    主程序;
    常数
    N=100000;
    变量
    i:整数;
    秒表:TStopwatch;
    名单:TList ;;
    记录:tEStrecord2;
    开始
    列表:=TList.Create;
    列表容量:=N;
    对于i:=0到N-1 do
    开始
    列表。添加(Rec);
    结束;
    秒表:=TStopwatch.StartNew;
    对于i:=0到N-1 do
    开始
    如果列表[i].Field3='abcde',则
    开始
    打破
    结束;
    结束;
    Writeln(秒表延时百万秒);
    结束;
    开始
    主要的;
    Readln;
    结束。
    
    我不得不将其编译为64位,以避免出现内存不足的情况。我的机器的输出大约是700。将
    List[i].Field3
    更改为
    List.List[i].Field3
    ,输出为单幅数字。时间安排相当粗糙,但我认为这证明了这一点


    大记录不利于缓存的问题仍然存在。这一点处理起来更为复杂,需要对真实世界的代码如何对其数据进行操作进行详细分析



    另一方面,如果您关心性能,则不会使用
    Extended
    。因为它的大小是10,而不是2的幂,所以内存访问经常会错误对齐。使用
    Double
    Real
    ,这是
    Double
    的别名

    考虑使用容量!提前为所有条目预先分配列表是有意义的。@ZENsan虽然您所说的一般都是正确的,但计时时间并不包括填充列表所花费的时间,只包括循环遍历列表所花费的时间。
    TTime
    不是一种特别准确的计时方式。改用。但45毫秒对于循环浏览列表中的5000项来说并不算长。可能在循环的中途出现了意外的暂停,例如任务切换、缓存未命中等
    {$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.