Delphi - 本地变量和TPair<Int,Int>数组 - 内存分配的奇怪行为

11
我可以帮您翻译成中文:我有以下代码示例,已在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;

如果我运行它,它会显示变量的地址。变量it在内存范围buf中有地址!当i达到3时,赋值buf[i].Value := 0;会覆盖i的前3个字节和t的最后一个字节。这导致无限循环,因为i每次达到3时都会被重置为0。如果我使用SetLength(buf,20);自己分配内存,一切都好。
图片展示了我的意思。
我的设置:
- Windows 7 64位 - Delphi XE 5 更新2 - 调试配置32位
奇怪,不是吗?有人能够复现吗?这是Delphi编译器的一个错误吗?
谢谢。
编辑:下面是相同的例子,可能更容易理解我的意思:
此外,对于我的糟糕英语感到抱歉 ;)

3
非常好的问题,非常清晰。有一个小建议。使用控制台应用程序,使用Writeln可以更容易地生成演示故障的短而完整的程序,并允许您获取可以粘贴为文本的文本输出。从而避免截图。@J...在他的答案中的程序是完美的例子。 - David Heffernan
谢谢David,我会记住这个,以备下次提问(或回答)。 - linluk
2个回答

11

这明显是一个编译器错误。它只影响在堆栈上分配的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个dword。第一个是整数i,只留下6个dword分配给TPair数组是不够的(SizeOf(TPair<integer,integer>)等于8 -> 两个dword)。在第三次迭代时,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开始仍会存在。


嗨,谢谢。我认为这完全是通用记录的问题。如果我使用相同的 TmyPair<T1,T2> = record Key:T1; Value:T2 end;,它会有相同的行为。看起来编译器在计算通用记录的实际大小时存在问题。 - linluk
无论如何,这证实了我的假设 :). 所以我接受了你的答案。 - linluk
2
非常好的分析。感谢您提供的QC报告。我添加了一个答案,其中包含一些替代解决方案,似乎更加健壮。 - David Heffernan

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.

同样地,这个也是:
{$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.

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

我正在测试其中一些用例,不知道在你出现之前我完成的机会有多大 ;) - J...
@J... 是的,我正在吃午餐!好问题。你做得很好,我喜欢你加入汇编来强调重点的方式。 - David Heffernan
同样地,我已经更新了QC,添加了其中一个示例作为额外信息和解决方法。 - J...
@linluk 嗯,我认为这是一种非常不同的方法,因为它涉及堆分配并且会显著改变程序的含义。我不认为任何解决方法特别吸引人。 - David Heffernan
2
我理解从您的角度来看,那是您个人选择解决面临问题的方式,我相信您做出了正确的选择。但是,我认为这个问题实际上是关于本地变量堆栈保留的,这就是错误所在。因此,针对这个问题,我宁愿不要涉及动态数组。 - David Heffernan
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接