Большая задержка при циклическом просмотре TList больших записей
Я использую Delphi 10.1 Berlin в Windows 10.
У меня есть две записи разных размеров. Я написал код, чтобы пройти через два TList<T>
из этих записей для проверки истекшего времени. Циклический просмотр списка больших записей выполняется намного медленнее.
Может кто-нибудь объяснить причину и предоставить решение для ускорения цикла?
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;
1 ответ
Прежде всего, я хочу рассмотреть весь код, даже код, который заполняет список, который, как я понимаю, вы не рассчитали. Поскольку вторая запись имеет больший размер, необходимо скопировать больше памяти при назначении этого типа записи. Кроме того, когда вы читаете из списка, более крупная запись менее дружественна к кэшу, чем более мелкая запись, что влияет на производительность. Этот последний эффект, вероятно, менее значительный, чем первый.
С этим связано то, что при добавлении элементов размер внутреннего массива списка должен быть изменен. Иногда изменение размера приводит к перераспределению, которое не может быть выполнено на месте. Когда это происходит, выделяется новый блок памяти, и предыдущий контент копируется в этот новый блок. Эта копия явно дорогая для большой записи. Вы можете уменьшить это, выделив массив один раз, если вы знаете его длину. Список Capacity
это механизм для использования. Конечно, не всегда вы будете знать длину заранее.
Ваша программа делает очень мало, кроме выделения памяти и доступа к памяти. Следовательно, производительность этих операций с памятью доминирует.
Теперь, ваш выбор времени - только код, который читает из списков. Таким образом, разница в производительности копирования памяти по заполнению не является частью выполненного вами теста. Ваши временные различия в основном сводятся к чрезмерному копированию памяти при чтении, как я объясню ниже.
Рассмотрим этот код:
if _List[i].Field3 = 'abcde' then
Так как _List[i]
это запись, тип значения, вся запись копируется в неявную скрытую локальную переменную. Код фактически эквивалентен:
var
tmp: tTestRecord2;
...
tmp := _List[i]; // copy of entire record
if tmp.Field3 = 'abcde' then
Есть несколько способов избежать этой копии:
- Измените базовый тип на ссылочный тип. Это изменяет требования к управлению памятью. И у вас может быть веская причина хотеть использовать тип значения.
- Используйте контейнерный класс, который может возвращать адрес элемента, а не копию элемента.
- Переключиться с
TList<T>
в динамический массивTArray<T>
, Это простое изменение позволит компилятору получать доступ к отдельным полям напрямую, не копируя целые записи. - Использовать
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
и вывод в отдельных цифрах. Время довольно грубое, но я думаю, что это демонстрирует смысл.
Проблема большой записи, не являющейся дружественной к кешу, остается С этим сложнее разобраться, и потребуется подробный анализ того, как код реального мира работает с его данными.
Кроме того, если вы заботитесь о производительности, то вы не будете использовать Extended
, Поскольку он имеет размер 10, а не степень двойки, доступ к памяти часто не совпадает. использование Double
или же Real
который является псевдонимом для Double
,