Delphi 循环浏览大型记录列表时的长延迟
我在Windows10中使用Delphi10.1 我有两张不同大小的唱片。我编写了代码,循环遍历这些记录中的两个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
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.