Почему динамический массив "конструктор" намного медленнее, чем SetLength и инициализация элементов?

Я сравнивал показатели между этими двумя способами инициализации динамического массива:

Arr := TArray<integer>.Create(1, 2, 3, 4, 5);

и

SetLength(Arr, 5);
Arr[0] := 1;
Arr[1] := 2;
Arr[2] := 3;
Arr[3] := 4;
Arr[4] := 5;

Я подготовил тест, и я заметил, что использование "конструктора" массива занимает в два раза больше времени, требуемого другим методом.

Тест:

uses
  DateUtils;

function CreateUsingSetLength() : TArray<integer>;
begin
  SetLength(Result, 5);
  Result[0] := 1;
  Result[1] := 2;
  Result[2] := 3;
  Result[3] := 4;
  Result[4] := 5;
end;

...

const
  C_COUNT = 10000000;
var
  Start : TDateTime;
  i : integer;
  Arr : TArray<integer>;
  MS1 : integer;
  MS2 : integer;
begin
  Start := Now;
  i := 0;
  while(i < C_COUNT) do
  begin
    Arr := TArray<integer>.Create(1, 2, 3, 4, 5);
    Inc(i);
  end;
  MS1 := MillisecondsBetween(Now, Start);

  Start := Now;
  i := 0;
  while(i < C_COUNT) do
  begin
    Arr := CreateUsingSetLength();
    Inc(i);
  end;
  MS2 := MillisecondsBetween(Now, Start);

  ShowMessage('Constructor = ' + IntToStr(MS1) + sLineBreak + 'Other method = ' + IntToStr(MS2));

Тестирование на моей машине, результирующие значения всегда близки к следующему:

Конструктор = 622

Другой метод = 288

Почему массив "конструктор" настолько медленный?

Ответ 1

Посмотрим на код, который был сгенерирован (оптимизация на, Win32 target, 10.2 Tokyo):

Project152.dpr.34: Arr := TArray<Integer>.Create(1, 2, 3, 4, 5);
004D0D22 8D45F8           lea eax,[ebp-$08]
004D0D25 8B15B84B4000     mov edx,[$00404bb8]
004D0D2B E858BFF3FF       call @DynArrayClear
004D0D30 6A05             push $05
004D0D32 8D45F8           lea eax,[ebp-$08]
004D0D35 B901000000       mov ecx,$00000001
004D0D3A 8B15B84B4000     mov edx,[$00404bb8]
004D0D40 E81FBEF3FF       call @DynArraySetLength
004D0D45 83C404           add esp,$04
004D0D48 8B45F8           mov eax,[ebp-$08]
004D0D4B C70001000000     mov [eax],$00000001
004D0D51 8B45F8           mov eax,[ebp-$08]
004D0D54 C7400402000000   mov [eax+$04],$00000002
004D0D5B 8B45F8           mov eax,[ebp-$08]
004D0D5E C7400803000000   mov [eax+$08],$00000003
004D0D65 8B45F8           mov eax,[ebp-$08]
004D0D68 C7400C04000000   mov [eax+$0c],$00000004
004D0D6F 8B45F8           mov eax,[ebp-$08]
004D0D72 C7401005000000   mov [eax+$10],$00000005
004D0D79 8B55F8           mov edx,[ebp-$08]
004D0D7C 8D45FC           lea eax,[ebp-$04]
004D0D7F 8B0DB84B4000     mov ecx,[$00404bb8]
004D0D85 E842BFF3FF       call @DynArrayAsg

и

Project152.dpr.12: SetLength(Result, 5);
004D0CB2 6A05             push $05
004D0CB4 8BC3             mov eax,ebx
004D0CB6 B901000000       mov ecx,$00000001
004D0CBB 8B15B84B4000     mov edx,[$00404bb8]
004D0CC1 E89EBEF3FF       call @DynArraySetLength
004D0CC6 83C404           add esp,$04
Project152.dpr.13: Result[0] := 1;
004D0CC9 8B03             mov eax,[ebx]
004D0CCB C70001000000     mov [eax],$00000001
Project152.dpr.14: Result[1] := 2;
004D0CD1 8B03             mov eax,[ebx]
004D0CD3 C7400402000000   mov [eax+$04],$00000002
Project152.dpr.15: Result[2] := 3;
004D0CDA 8B03             mov eax,[ebx]
004D0CDC C7400803000000   mov [eax+$08],$00000003
Project152.dpr.16: Result[3] := 4;
004D0CE3 8B03             mov eax,[ebx]
004D0CE5 C7400C04000000   mov [eax+$0c],$00000004
Project152.dpr.17: Result[4] := 5;
004D0CEC 8B03             mov eax,[ebx]
004D0CEE C7401005000000   mov [eax+$10],$00000005

Итак, понятно, что код, созданный для вызова "конструктор" , просто оптимизирован.

Как вы можете видеть, сначала код "конструктор" очищает, выделяет и заполняет анонимный массив (at [ebp-$08]), а в конце присваивает его переменной Arr (at [ebp-$04]). В основном это происходит медленнее.

В более новых версиях существует третий способ:

Arr := [1, 2, 3, 4, 5];

Но это создает тот же самый код, что и синтаксис "конструктор" . Но вы можете ускорить это:

const
  C_ARR = [1, 2, 3, 4, 5]; // yes, dynarray const!

и

Arr := C_ARR;

Это просто генерирует динамический массив один раз, с подсчетом ссылок -1 и в цикле просто выполняет присваивание:

Project152.dpr.63: Arr := C_ARR;
004D0E60 8D45FC           lea eax,[ebp-$04]
004D0E63 8B15C4864D00     mov edx,[$004d86c4]
004D0E69 8B0DB84B4000     mov ecx,[$00404bb8]
004D0E6F E858BEF3FF       call @DynArrayAsg

Замечание:

Но, как прокомментировал @DavidHeffernan, в реальном программировании эти различия в производительности вряд ли когда-либо будут замечены. Обычно вы не инициализируете такие массивы в жестких циклах, и в разовой ситуации разница составляет несколько наносекунд, которые вы не заметите в течение всего запуска программы.

Замечание 2:

Кажется, есть какая-то путаница. Тип TArray<Integer> точно такой же, как array of Integer. Для динамических массивов также классы или некоторые другие оболочки. Это простые динамические массивы и ничего больше. Синтаксис конструктора может применяться к обоим. Единственное различие заключается в совместимости типов. TArray<Integer> может использоваться как объявление типа ad-hoc, а все TArray<Integer> совместимы с типом.