delphi - локална променлива и масив от TPair‹Int,Int› - странно поведение на разпределението на паметта

имам следния примерен код, компилиран в delphi xe5 актуализация 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 винаги се нулира до 0, когато достигне 3.
ако разпределя паметта сам с SetLength(buf,20);, всичко е наред.

снимката показва какво имам предвид.

Изход, адреси на паметта

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

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

странно, нали?
някой може ли да го възпроизведе?
дали е грешка в компилатора на delphi?

Благодаря.

РЕДАКТИРАНЕ:
ето същия пример, но може би е по-добре да разберете какво имам предвид: зони на паметта

и между другото: съжалявам за лошия ми английски ;)


person linluk    schedule 12.08.2014    source източник
comment
+1 Много добър въпрос, много ясен. Един малък коментар. Използването на конзолни приложения с Writeln улеснява създаването на кратки, пълни програми, които демонстрират грешката. И ви позволяват да получите текстов изход, който може да бъде поставен като текст. По този начин се избягват екранни снимки. Програмите на @J... в неговия отговор са идеални примери.   -  person David Heffernan    schedule 12.08.2014
comment
благодаря Дейвид, ще го имам предвид за следващия си въпрос (или отговор).   -  person linluk    schedule 12.08.2014


Отговори (2)


Това определено изглежда като грешка в компилатора. Той засяга само масив от 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 dwords в стека. Първият е за цялото число i, оставяйки само 6 двойни думи, заделени за масива 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, ако и вие виждате проблема.

person J...    schedule 12.08.2014
comment
Здравей, благодаря. мисля, че изобщо е проблемът с общите записи. ако направя същото с TmyPair<T1,T2> = record Key:T1; Value:T2 end;, то има същото поведение. изглежда, че компилаторът има проблеми с изчисляването на реалния размер на общ запис. - person linluk; 12.08.2014
comment
така или иначе потвърждава моето предположение :). така че приех отговора ти. - person linluk; 12.08.2014
comment
+1 Добре анализирано. И благодаря за QC доклада. Добавих отговор с някои алтернативни решения, които изглеждат малко по-стабилни. - person David Heffernan; 12.08.2014

Ясно е, че @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.

Така че изглежда, че докато компилаторът вече е инстанцирал генеричния тип, преди да срещне декларацията на локалната променлива, той може да резервира правилния размер на стека.

person David Heffernan    schedule 12.08.2014
comment
Бях на етап тестване на някои от тези случаи и се чудех какви са шансовете ми да завърша, преди да се появиш ти ;) - person J...; 12.08.2014
comment
@J... Да, обядвах! Хубав въпрос. Свършихте добра работа и ми харесва вашето включване на asm, за да подчертаете идеята. - person David Heffernan; 12.08.2014
comment
По същия начин. Актуализирах QC, за да добавя един от тези примери като допълнителна информация и заобиколно решение. - person J...; 12.08.2014
comment
Бих включил и динамични масиви като заобиколно решение. за мен това беше най-лесният/най-бързият начин просто да декларирам buf: array of TPair<Integer,Integer> и да извикам SetLength(buf,20);. Не искам да дефинирам типове за всяка комбинация от типове данни (като TPair<Integer,Integer, TPair<Double,Integer и т.н.), защото използвам TPair много често. - person linluk; 12.08.2014
comment
@linluk Хмм. Мисля, че това е доста драстично различен подход, защото това включва разпределение на купчина и наистина променя значително значението на вашата програма. Не мисля, че някое от заобиколните решения е особено привлекателно. - person David Heffernan; 12.08.2014
comment
@DavidHeffernan, добре, от тази гледна точка си абсолютно прав. но в реалния живот предпочитам да използвам динамичен масив (третиран като фиксиран в следния код), вместо да дефинирам всяка комбинация, от която се нуждая. - person linluk; 12.08.2014
comment
Разбирам това от твоя гледна точка. Така вие лично решавате проблема, пред който сте изправени и съм сигурен, че сте направили правилния избор. Но мисля, че този въпрос наистина е за резервиране на стек за локални променливи, там се крие грешката. Така че за целите на този въпрос, бих предпочел да не включвам динамични масиви в микса. - person David Heffernan; 12.08.2014
comment
добър избор! аз също не бих го направил сега, след като разбрах мнението ти. - person linluk; 12.08.2014