为什么 MSVC 调试模式会为一个空的 if() 主体而不是另一个(i++ 与 ++i)省略 cmp/jcc?
Why does MSVC debug mode leave out the cmp/jcc for one empty if() body but not another (i++ vs. ++i)?
我在用一台AMD64位的电脑(Intel Pentium Gold 4415U)对比一些C语言转过来的汇编指令(当然是反汇编)
使用 Windows 10,我使用 Visual Studio 2017(15.2) 和他们的 C 编译器。
我的示例代码如下所示:
int main() {
int i = 0;
if(++i == 4);
if(i++ == 4);
return 0;
}
反汇编如下:
mov eax,dword ptr [i] // if (++i == 4);
inc eax
mov dword ptr [i],eax
mov eax,dword ptr [i] // if (i++ == 4);
mov dword ptr [rbp+0D4h],eax ; save old i to a temporary
mov eax,dword ptr [i]
inc eax
mov dword ptr [i],eax
cmp dword ptr [rbp+0D4h],4 ; compare with previous i
jne main+51h (07FF7DDBF3601h)
mov dword ptr [rbp+0D8h],1
jmp main+5Bh (07FF7DDBF360Bh)
*mov dword ptr [rbp+0D8h],0
07FF7DDBF3601 转到最后一行指令(*)。
07FF7DDBF360B 转到 'return 0;'。
在if (++i == 4)
中,程序不观察'added' i 是否满足条件
但是在if (i++ == 4)
中,程序将'previous'i保存到栈中,然后进行自增。之后,程序将 'previous' i 与常数整数 4.
进行比较
两个C代码不同的原因是什么?它只是编译器的机制吗?更复杂的代码会有所不同吗?
我试图用 Google 找到这个,但是我没能找到差异的根源。必须我明白'This is just a compiler behavior'?
正如 Paul 所说,该程序没有可观察到的副作用,并且启用优化的 MSVC 或任何其他主要编译器 (gcc/clang/ICC) 会将 main
简单地编译为 xor eax,eax
/ ret
.
i
的值永远不会逃脱函数(不存储到全局或返回),因此它可以完全优化掉。即使是这样,恒定传播在这里也是微不足道的。
MSVC 的调试模式反优化代码生成决定不在空的 if
主体上发出 cmp/jcc
这只是一个怪癖/实现细节;即使在调试模式下也根本无助于调试。这将是一个分支指令,跳转到它落入的相同地址。
调试模式代码的要点在于您可以单步执行 源代码行,并使用调试器修改 C 变量。并不是说 asm 是 C 到 asm 的字面和忠实的音译。 (而且编译器可以快速生成它,无需在质量上花费任何精力,以加快 edit/compile/run 周期。)Why does clang produce inefficient asm with -O0 (for this simple floating point sum)?
编译器的代码生成到底有多脑残不取决于任何语言规则;没有实际的标准来定义编译器在调试模式下必须为空 if
主体实际使用分支指令。
显然对于您的编译器版本,i++
post-增量足以让编译器忘记循环体是空的?
我无法使用 MSVC 19.0 或 19.10 重现您的结果 on the Godbolt compiler explorer, with 32 or 64-bit mode。 (VS2015 或 VS2017)。或任何其他 MSVC 版本。我根本没有从 MSVC、ICC 或 gcc 获得条件分支。
MSVC 确实实现了 i++
并在内存中实际存储了旧值,就像您展示的那样。太坏了。 GCC -O0
使调试模式代码更加高效。当然仍然很脑残,但在一个单一的声明中,有时它会好得多。
我可以用clang重现它,不过! (但它对两个 if
s 都有分支):
# clang8.0 -O0
main: # @main
push rbp
mov rbp, rsp
mov dword ptr [rbp - 4], 0 # default return value
mov dword ptr [rbp - 8], 0 # int i=0;
mov eax, dword ptr [rbp - 8]
add eax, 1
mov dword ptr [rbp - 8], eax
cmp eax, 4 # uses the i++ result still in a register
jne .LBB0_2 # jump over if() body
jmp .LBB0_2 # jump over else body, I think.
.LBB0_2:
mov eax, dword ptr [rbp - 8]
mov ecx, eax
add ecx, 1 # i++ uses a 2nd register
mov dword ptr [rbp - 8], ecx
cmp eax, 4
jne .LBB0_4
jmp .LBB0_4
.LBB0_4:
xor eax, eax # return 0
pop rbp # tear down stack frame.
ret
我在用一台AMD64位的电脑(Intel Pentium Gold 4415U)对比一些C语言转过来的汇编指令(当然是反汇编)
使用 Windows 10,我使用 Visual Studio 2017(15.2) 和他们的 C 编译器。 我的示例代码如下所示:
int main() {
int i = 0;
if(++i == 4);
if(i++ == 4);
return 0;
}
反汇编如下:
mov eax,dword ptr [i] // if (++i == 4);
inc eax
mov dword ptr [i],eax
mov eax,dword ptr [i] // if (i++ == 4);
mov dword ptr [rbp+0D4h],eax ; save old i to a temporary
mov eax,dword ptr [i]
inc eax
mov dword ptr [i],eax
cmp dword ptr [rbp+0D4h],4 ; compare with previous i
jne main+51h (07FF7DDBF3601h)
mov dword ptr [rbp+0D8h],1
jmp main+5Bh (07FF7DDBF360Bh)
*mov dword ptr [rbp+0D8h],0
07FF7DDBF3601 转到最后一行指令(*)。
07FF7DDBF360B 转到 'return 0;'。
在if (++i == 4)
中,程序不观察'added' i 是否满足条件
但是在if (i++ == 4)
中,程序将'previous'i保存到栈中,然后进行自增。之后,程序将 'previous' i 与常数整数 4.
两个C代码不同的原因是什么?它只是编译器的机制吗?更复杂的代码会有所不同吗?
我试图用 Google 找到这个,但是我没能找到差异的根源。必须我明白'This is just a compiler behavior'?
正如 Paul 所说,该程序没有可观察到的副作用,并且启用优化的 MSVC 或任何其他主要编译器 (gcc/clang/ICC) 会将 main
简单地编译为 xor eax,eax
/ ret
.
i
的值永远不会逃脱函数(不存储到全局或返回),因此它可以完全优化掉。即使是这样,恒定传播在这里也是微不足道的。
MSVC 的调试模式反优化代码生成决定不在空的 if
主体上发出 cmp/jcc
这只是一个怪癖/实现细节;即使在调试模式下也根本无助于调试。这将是一个分支指令,跳转到它落入的相同地址。
调试模式代码的要点在于您可以单步执行 源代码行,并使用调试器修改 C 变量。并不是说 asm 是 C 到 asm 的字面和忠实的音译。 (而且编译器可以快速生成它,无需在质量上花费任何精力,以加快 edit/compile/run 周期。)Why does clang produce inefficient asm with -O0 (for this simple floating point sum)?
编译器的代码生成到底有多脑残不取决于任何语言规则;没有实际的标准来定义编译器在调试模式下必须为空 if
主体实际使用分支指令。
显然对于您的编译器版本,i++
post-增量足以让编译器忘记循环体是空的?
我无法使用 MSVC 19.0 或 19.10 重现您的结果 on the Godbolt compiler explorer, with 32 or 64-bit mode。 (VS2015 或 VS2017)。或任何其他 MSVC 版本。我根本没有从 MSVC、ICC 或 gcc 获得条件分支。
MSVC 确实实现了 i++
并在内存中实际存储了旧值,就像您展示的那样。太坏了。 GCC -O0
使调试模式代码更加高效。当然仍然很脑残,但在一个单一的声明中,有时它会好得多。
我可以用clang重现它,不过! (但它对两个 if
s 都有分支):
# clang8.0 -O0
main: # @main
push rbp
mov rbp, rsp
mov dword ptr [rbp - 4], 0 # default return value
mov dword ptr [rbp - 8], 0 # int i=0;
mov eax, dword ptr [rbp - 8]
add eax, 1
mov dword ptr [rbp - 8], eax
cmp eax, 4 # uses the i++ result still in a register
jne .LBB0_2 # jump over if() body
jmp .LBB0_2 # jump over else body, I think.
.LBB0_2:
mov eax, dword ptr [rbp - 8]
mov ecx, eax
add ecx, 1 # i++ uses a 2nd register
mov dword ptr [rbp - 8], ecx
cmp eax, 4
jne .LBB0_4
jmp .LBB0_4
.LBB0_4:
xor eax, eax # return 0
pop rbp # tear down stack frame.
ret