Delphi - локальная переменная и массив TPair <Int, Int> - странное поведение выделения памяти

У меня есть следующий пример кода, скомпилированный в delphi xe5 update 2.

procedure TForm1.FormCreate(Sender: TObject);
var i,t:Integer;
    buf: array [0..20] of TPair<Integer,Integer>;
begin
  t := 0;
  for i := Low(buf) to High(buf) do begin
    ShowMessage(
      Format(
        'Pointer to i = %p;'#$d#$a+
        'Pointer to buf[%d].Key = %p;'#$d#$a+
        'Pointer to buf[%d].Value = %p;'#$d#$a+
        'Pointer to t = %p',
        [@i, i, @(buf[i].Key), i, @(buf[i].Value), @t]
      )
    );
    buf[i].Key := 0;
    buf[i].Value := 0;
    t := t + 1;
  end;
end;

если я запустил его, он показывает мне адреса переменных. переменные i и t имеют адреса в диапазоне памяти buf!
когда i достигает 3, присваивание buf[i].Value := 0; перезаписывает первые 3 байта i и последний байт t. это приводит к циклу бесконечности, потому что i получает все reset до 0, когда он достигает 3.
если я сам выделяю память с помощью SetLength(buf,20);, все в порядке.

изображение показывает, что я имею в виду.

Output, Memory Adresses

моя настройка:

  • Windows 7 64 бит
  • Обновление Delphi XE 5 2
  • Конфигурация отладки 32 бит

странно, не так ли? может ли кто-нибудь воспроизвести его?
это ошибка в компиляторе delphi?

спасибо.

EDIT:
вот тот же пример, но, возможно, лучше понять, что я имею в виду: memory areas

и btw: извините за мой плохой английский;)

Ответ 1

Это определенно похоже на ошибку компилятора. Он влияет только на массив TPair, выделенный в стеке. Например, это компилируется и выполняется отлично:

program Project1;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  Generics.Collections;

var i:Integer;
    buf: array [0..20] of TPair<Integer,Integer>;
begin
  for i := Low(buf) to High(buf) do begin
    buf[i].Key := 0;
    buf[i].Value := 0;
  end;
end.

Это, однако, демонстрирует ошибку:

program Project1;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  Generics.Collections;    

procedure DoSomething;
var i:Integer;
    buf: array [0..20] of TPair<Integer,Integer>;
begin
  for i := Low(buf) to High(buf) do begin
    buf[i].Key := 0;
    buf[i].Value := 0;
  end;
end;

begin
  DoSomething;
end.

Компилятор, по-видимому, просчитывает размер TPair<Integer,Integer>. Скомпилированная сборка показывает преамбулу следующим образом:

Project1.dpr.14: begin
00445C50 55               push ebp
00445C51 8BEC             mov ebp,esp
00445C53 83C4E4           add esp,-$1c  //***  Allocate only 28 bytes (7words)
Project1.dpr.15: for i := Low(buf) to High(buf) do begin
00445C56 33C0             xor eax,eax
00445C58 8945FC           mov [ebp-$04],eax
Project1.dpr.16: buf[i].Key := 0;
00445C5B 8B45FC           mov eax,[ebp-$04]
00445C5E 33D2             xor edx,edx
00445C60 8954C5E7         mov [ebp+eax*8-$19],edx
Project1.dpr.17: buf[i].Value := 0;
00445C64 8B45FC           mov eax,[ebp-$04]
00445C67 33D2             xor edx,edx
00445C69 8954C5EB         mov [ebp+eax*8-$15],edx
Project1.dpr.18: end;
00445C6D FF45FC           inc dword ptr [ebp-$04]
Project1.dpr.15: for i := Low(buf) to High(buf) do begin
00445C70 837DFC15         cmp dword ptr [ebp-$04],$15
00445C74 75E5             jnz $00445c5b
Project1.dpr.19: end;
00445C76 8BE5             mov esp,ebp
00445C78 5D               pop ebp
00445C79 C3               ret 
00445C7A 8BC0             mov eax,eax

Компилятор выделил только 7 слов в стеке. Первый - для целого числа i, оставляя только 6 dwords, выделенных для массива TPair, которого недостаточно (SizeOf(TPair<integer,integer>) равно 8 → два слова). На третьей итерации mov [ebp+eax*8-$15],edx (т.е.: buf[2].Value) запускается в местоположение стека для i и устанавливает его значение в ноль.

Вы можете продемонстрировать рабочую программу, заставив достаточно места в стеке:

program Project1;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  Generics.Collections;


procedure DoSomething;
var i:Integer;
    fixalloc : array[0..36] of Integer; // dummy variable
                                        // allocating enough space for
                                        // TPair array
    buf: array [0..20] of TPair<Integer,Integer>;
begin
  for i := Low(buf) to High(buf) do begin
    buf[i].Key := i;
    buf[i].Value := i;
  end;
end;

begin
  DoSomething;
end.

Это проверено в XE2, но похоже, что это продолжается по крайней мере в XE5, если вы также видите проблему.

Ответ 2

Понятно, что @J... корректно идентифицирует это как ошибку компилятора. Из моих тестов я наблюдаю, что он затрагивает 32 и 64-битные версии Windows компилятора. Я не знаю о компиляторе OSX или мобильных компиляторах.

Есть некоторые разумные обходные пути. Эта проблема дает разумный результат:

{$APPTYPE CONSOLE}
uses
  System.SysUtils, Generics.Collections;

type
  TFixedLengthPairArray = array [0..20] of TPair<Integer,Integer>;

procedure DoSomething;
var
  i: Integer;
  buf: TFixedLengthPairArray;
begin
  Writeln(Format('%p %p', [@i, @buf]));
end;

begin
  DoSomething;
end.

Аналогично:

{$APPTYPE CONSOLE}
uses
  System.SysUtils, Generics.Collections;

type
  TFixedLengthPairArray = array [0..20] of TPair<Integer,Integer>;

procedure DoSomething;
var
  i: Integer;
  buf: array [0..20] of TPair<Integer,Integer>;
begin
  Writeln(Format('%p %p', [@i, @buf]));
end;

begin
  DoSomething;
end.

Или действительно это:

{$APPTYPE CONSOLE}
uses
  System.SysUtils, Generics.Collections;

type
  TPairOfIntegers = TPair<Integer,Integer>;

procedure DoSomething;
var
  i: Integer;
  buf: array [0..20] of TPairOfIntegers;
begin
  Writeln(Format('%p %p', [@i, @buf]));
end;

begin
  DoSomething;
end.

И даже это:

{$APPTYPE CONSOLE}
uses
  System.SysUtils, Generics.Collections;

type
  TPairOfIntegers = TPair<Integer,Integer>;

procedure DoSomething;
var
  i: Integer;
  buf: array [0..20] of TPair<Integer,Integer>;
begin
  Writeln(Format('%p %p', [@i, @buf]));
end;

begin
  DoSomething;
end.

И это:

{$APPTYPE CONSOLE}
uses
  System.SysUtils, Generics.Collections;

procedure DoSomething;
type
  TPairOfIntegers = TPair<Integer,Integer>;
var
  i: Integer;
  buf: array [0..20] of TPair<Integer,Integer>;
begin
  Writeln(Format('%p %p', [@i, @buf]));
end;

begin
  DoSomething;
end.

Таким образом, кажется, что до тех пор, пока компилятор уже создавал общий тип, прежде чем он встретит объявление локальной переменной, он сможет зарезервировать правильный размер стека.