Delphi 优化:常量循环

6
我刚刚注意到我正在编写的一个程序中有一些非常有趣的东西。我有一个简单的过程,用 x 类型的对象填充 TStringlist。
当我追踪问题时添加了一个断点,发现指针从 12 开始下降到 1,我希望有人能够解释为什么会发生这种情况或者提供相关文档链接,因为我找不到任何解释。
我的循环从 0 到 11,我在循环中使用的指针被初始化为 nPtr := 0,但在运行程序时,nPtr 变量从 12 开始下降到 1。然后我在循环外部初始化了该变量,如代码片段所示,但是同样的事情仍然发生。该变量在该单元中没有被其他地方使用。
我询问了其中的一位同事,他说这是由于 Delphi 优化造成的,但我想知道它是如何决定影响哪个循环以及为什么会发生这种情况。
感谢您的帮助。
代码:
procedure TUnit.ProcedureName;
var
    nPtr : Integer;
    obj : TObject;
begin
nPtr:=0;//added later
for nPtr := 0 to 11 do
    begin
    obj := TObject.Create(Self);
    slPeriodList.AddObject('X', obj);
    end;
end;

2
这是调试器中的一个限制(或错误,取决于您的态度)。David在他的回答中解释了编译器正在做什么,这很好,但在我看来,调试器应该真正知道已执行此转换并且应该为监视变量等反向(消除)可观察效果。然而,它没有这样做。 - 500 - Internal Server Error
1
我认为从调试器那里要求太多了。它要么显示可怕的消息“由于优化,nPtr不可访问”,要么显示实际值,向您展示引擎盖下发生了什么。我更喜欢后者(这也是它现在所做的)。调试器的任务不是使循环索引看起来像上升了,而实际上并没有。 - Rudy Velthuis
1个回答

8

只有当循环体不引用循环变量时,优化才是可能的。在这种情况下,如果循环的下限为零,则编译器将反转循环。

如果循环变量从未被循环体引用,则编译器有权以任何方式实现循环。它所需要做的就是按照循环边界所规定的次数执行循环体。实际上,编译器可以完全合理地优化掉循环变量。

考虑以下程序:

{$APPTYPE CONSOLE}

procedure Test1;
var
  i: Integer;
begin
  for i := 0 to 11 do
    Writeln(0);
end;

procedure Test2;
var
  i: Integer;
begin
  for i := 0 to 11 do
    Writeln(i);
end;

begin
  Test1;
  Test2;
end.
Test1的主体是由XE7、32位Windows编译器以发布选项编译成以下代码的:
Project1.dpr.9: for i := 0 to 11 do
00405249 BB0C000000       mov ebx,$0000000c
Project1.dpr.10: Writeln(0);
0040524E A114784000       mov eax,[$00407814]
00405253 33D2             xor edx,edx
00405255 E8FAE4FFFF       call @Write0Long
0040525A E8D5E7FFFF       call @WriteLn
0040525F E800DBFFFF       call @_IOTest
Project1.dpr.9: for i := 0 to 11 do
00405264 4B               dec ebx
00405265 75E7             jnz $0040524e
编译器向下运行循环,可以通过使用dec看到。请注意,在没有cmp的情况下使用jnz进行循环终止测试。这是因为dec执行了隐式的零比较。 dec的文档如下所示:

受影响的标志

CF标志不受影响。OF、SF、ZF、AF和PF标志根据结果设置。

当且仅当dec指令的结果为零时,ZF标志被设置。而ZF决定了jnz是否跳转。 Test2的代码如下:
Project1.dpr.17: for i := 0 to 11 do
0040526D 33DB             xor ebx,ebx
Project1.dpr.18: Writeln(i);
0040526F A114784000       mov eax,[$00407814]
00405274 8BD3             mov edx,ebx
00405276 E8D9E4FFFF       call @Write0Long
0040527B E8B4E7FFFF       call @WriteLn
00405280 E8DFDAFFFF       call @_IOTest
00405285 43               inc ebx
Project1.dpr.17: for i := 0 to 11 do
00405286 83FB0C           cmp ebx,$0c
00405289 75E4             jnz $0040526f
请注意,循环变量正在增加,并且我们现在有一个额外的cmp指令,在每次循环迭代中执行。
也许有趣的是,64位Windows编译器不包括此优化。对于Test1,它会产生以下结果:
我不确定为什么这个优化没有在64位编译器中实现。我的猜测是这个优化在实际应用中的效果微乎其微,设计者们选择不花费精力在64位编译器上实现它。需要翻译的内容如下:
Project1.dpr.9: for i := 0 to 11 do
00000000004083A5 4833DB           xor rbx,rbx
Project1.dpr.10: Writeln(0);
00000000004083A8 488B0D01220000   mov rcx,[rel $00002201]
00000000004083AF 4833D2           xor rdx,rdx
00000000004083B2 E839C3FFFF       call @Write0Long
00000000004083B7 4889C1           mov rcx,rax
00000000004083BA E851C7FFFF       call @WriteLn
00000000004083BF E86CB4FFFF       call @_IOTest
00000000004083C4 83C301           add ebx,$01
Project1.dpr.9: for i := 0 to 11 do
00000000004083C7 83FB0C           cmp ebx,$0c
00000000004083CA 75DC             jnz Test1 + $8

@RudyVelthuis 这也需要 A 是一个未被捕获的局部变量。你看到的是公平的。我不认为编译器在实践中会分析任何类似的东西。我们实际上没有像 C/C++ 的“as-if”规则这样正式的东西。 - David Heffernan
如果A在循环外可见,则顺序很重要。在循环外可见意味着循环体执行顺序可以观察到。因此,如果A是一个全局变量,那么编译器无法证明重新排序循环执行不会改变可观察行为。 - David Heffernan
1
@Rudy和David,就这样简单地引用数组元素而言,编译器会预先计算第一个受影响元素的地址,然后根据元素大小进行add(或在downto循环中使用sub)。换句话说,它甚至不使用循环控制变量来索引数组。 - Tom Brunberg
David,如果Rudy说元素的访问顺序与循环描述的顺序不同,那么这是我从未见过的。 - Tom Brunberg
David,这就是我所说的:MainFormU.pas.122: for x := 2 to 3 do 005E4B24 BA02000000 mov edx,$00000002 005E4B29 B87C975F00 mov eax,$005f977c MainFormU.pas.123: arr[x2] := 99; 005E4B2E C70063000000 mov [eax],$00000063 005E4B34 83C008 add eax,$08 MainFormU.pas.122: for x := 2 to 3 do 005E4B37 4A dec edx 005E4B38 75F4 jnz $005e4b2e MainFormU.pas.124: end; 005E4B3A C3 ret 。edx是循环计数器,eax预先计算为指向第一个受影响的元素,然后通过添加aex,$08(因为[x2])进行修改。 - Tom Brunberg
显示剩余15条评论

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