Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is dynamic array "constructor" much slower than SetLength and elements initialization?

I was comparing performances between these two ways of initializing a dynamic array:

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

and

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

I've prepared a test and I've noticed that using the array "constructor" takes twice the time required by the other method.

Test:

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));

Testing on my machine, the resulting values are always near the following:

Constructor = 622

Other method = 288

Why is the array "constructor" so slow?

like image 596
Fabrizio Avatar asked Aug 20 '17 19:08

Fabrizio


1 Answers

Let's take a look at the code that was generated (optimization on, 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

and:

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

So it is clear that the code generated for the "constructor" call is simply less optimized.

As you can see, the "constructor" code first clears, allocates and fills an anonymous array (at [ebp-$08]) and at the end, assigns that to the Arr variable (at [ebp-$04]). That is mainly why it is slower.

In newer versions, there is a third way:

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

But this produces the exact same code as the "constructor" syntax. But you can speed this up with:

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

and

Arr := C_ARR;

This simply generates the dynamic array once, with a reference count of -1, and in the loop, simply does an assignment (well, in _DynArrayAsg, actually a copy -- but this is still faster):

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

REMARK:

But, as @DavidHeffernan commented, in real life programming, these performance differences will hardly ever be noticed. You generally don't initialize such arrays in tight loops, and in a one-off situation, the difference is a few nanoseconds, which you won't notice during the entire run of the program.

REMARK 2:

There seems to be some confusion. The type TArray<Integer> is exactly the same as an array of Integer. Neither are classes or some other kind of wrappers for dynamic arrays. They are plain dynamic arrays and nothing else. The constructor syntax can be applied to both. The only difference is in type compatibility. TArray<Integer> can be used as an ad-hoc type declaration, and all TArray<Integer> are type-compatible.

like image 151
Rudy Velthuis Avatar answered Oct 28 '22 01:10

Rudy Velthuis