delphi - 局部变量和TPair <Int,Int>数组 - 内存分配的奇怪行为

lin*_*luk 10 delphi local-variables delphi-xe5

我有以下代码示例在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;
Run Code Online (Sandbox Code Playgroud)

如果我运行它它会向我显示变量的地址.变量it在内存范围内有地址buf!
i达到3时,赋值buf[i].Value := 0;覆盖前3个字节i和最后一个字节t.这会产生一个无限循环,因为它i0在到达时全部重置3.
如果我自己分配内存,SetLength(buf,20);一切都很好.

图为我的意思.

输出,内存地址

我的设置:

  • Windows 7 64位
  • Delphi XE 5 Update 2
  • 调试配置32位

奇怪,不是吗?
任何人都可以复制它吗?
它是delphi编译器中的一个错误吗?

谢谢.

编辑:
这是相同的例子,但也许更好地理解我的意思: 记忆区

顺便说一句:抱歉我的英文不好;)

J..*_*... 10

这绝对看起来像编译器错误.它只影响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.
Run Code Online (Sandbox Code Playgroud)

但是,这证明了错误:

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.
Run Code Online (Sandbox Code Playgroud)

编译器似乎错误地计算了a的大小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
Run Code Online (Sandbox Code Playgroud)

编译器在堆栈上只分配了7个双字.第一个是整数i,只为该TPair数组分配6个双字,这是不够的(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.
Run Code Online (Sandbox Code Playgroud)

这是在XE2中测试的,但如果你也看到问题,它似乎至少在XE5中持续存在.

  • @linluk QC提交:http://qc.embarcadero.com/wc/qcmain.aspx?d = 126899 (3认同)
  • +1好分析.并感谢质量控制报告.我添加了一些答案,其中包含一些看似更强大的替代解决方法. (2认同)

Dav*_*nan 5

很明显,@ 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.
Run Code Online (Sandbox Code Playgroud)

同样这个:

{$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.
Run Code Online (Sandbox Code Playgroud)

或者确实如此:

{$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.
Run Code Online (Sandbox Code Playgroud)

甚至这个:

{$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.
Run Code Online (Sandbox Code Playgroud)

还有这个:

{$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.
Run Code Online (Sandbox Code Playgroud)

所以似乎只要编译器在遇到局部变量声明之前已经实例化了泛型类型,它就能够保留正确的堆栈大小.

  • 我从你的角度理解这一点.这就是你个人选择解决你面临的问题的方式,我相信你做出了正确的选择.但我认为这个问题实际上是关于局部变量的堆栈预留,这就是bug的所在.所以出于这个问题的目的,我宁愿不把动态数组加入到混合中. (2认同)