两个循环都是无限循环,但我们可以看出每次迭代哪一个需要更多的指令/资源。
使用gcc编译器,我将以下两个程序编译为汇编代码,并进行了不同级别的优化:
int main(void) {
while(1) {}
return 0;
}
int main(void) {
while(2) {}
return 0;
}
即使没有进行任何优化(
-O0
),
这两个程序生成的汇编代码是完全相同的。因此,这两个循环之间没有速度差异。
参考以下生成的汇编代码(使用带有优化标志的
gcc main.c -S -masm=intel
):
使用
-O0
:
.file "main.c"
.intel_syntax noprefix
.def __main
.text
.globl main
.def main
.seh_proc main
main:
push rbp
.seh_pushreg rbp
mov rbp, rsp
.seh_setframe rbp, 0
sub rsp, 32
.seh_stackalloc 32
.seh_endprologue
call __main
.L2:
jmp .L2
.seh_endproc
.ident "GCC: (tdm64-2) 4.8.1"
使用-O1
选项:
.file "main.c"
.intel_syntax noprefix
.def __main
.text
.globl main
.def main
.seh_proc main
main:
sub rsp, 40
.seh_stackalloc 40
.seh_endprologue
call __main
.L2:
jmp .L2
.seh_endproc
.ident "GCC: (tdm64-2) 4.8.1"
使用-O2
和-O3
(相同的输出):
.file "main.c"
.intel_syntax noprefix
.def __main
.section .text.startup,"x"
.p2align 4,,15
.globl main
.def main
.seh_proc main
main:
sub rsp, 40
.seh_stackalloc 40
.seh_endprologue
call __main
.L2:
jmp .L2
.seh_endproc
.ident "GCC: (tdm64-2) 4.8.1"
实际上,对于每个优化级别生成的循环汇编代码是相同的:
.L2:
jmp .L2
.seh_endproc
.ident "GCC: (tdm64-2) 4.8.1"
重要的部分如下:
.L2:
jmp .L2
我不太擅长阅读汇编语言,但这显然是一个无条件循环。jmp
指令无条件地将程序重置回到.L2
标签,甚至不会将一个值与真值进行比较,当然,它会立即再次执行,直到程序以某种方式结束。这直接对应于C/C++代码:
L2:
goto L2;
编辑:
有趣的是,即使没有任何优化,下列循环在汇编中都产生了完全相同的输出(无条件jmp
):
while(42) {}
while(1==1) {}
while(2==2) {}
while(4<7) {}
while(3==3 && 4==4) {}
while(8-9 < 0) {}
while(4.3 * 3e4 >= 2 << 6) {}
while(-0.1 + 02) {}
让我感到惊讶的是:
#include<math.h>
while(sqrt(7)) {}
while(hypot(3,4)) {}
用户定义的函数会使情况变得更有趣:
int x(void) {
return 1;
}
while(x()) {}
#include<math.h>
double x(void) {
return sqrt(7);
}
while(x()) {}
在 -O0
的情况下,这两个示例实际上会每次调用 x
并进行比较。
第一个示例(返回 1):
.L4:
call x
testl %eax, %eax
jne .L4
movl $0, %eax
addq $32, %rsp
popq %rbp
ret
.seh_endproc
.ident "GCC: (tdm64-2) 4.8.1"
第二个例子(返回sqrt(7)
):
.L4:
call x
xorpd %xmm1, %xmm1
ucomisd %xmm1, %xmm0
jp .L4
xorpd %xmm1, %xmm1
ucomisd %xmm1, %xmm0
jne .L4
movl $0, %eax
addq $32, %rsp
popq %rbp
ret
.seh_endproc
.ident "GCC: (tdm64-2) 4.8.1"
然而,在-O1
及以上级别,它们都会产生与之前示例相同的汇编代码(无条件跳转到前一个标签)。
TL;DR
在GCC下,不同的循环被编译为相同的汇编代码。编译器对常量值进行评估,并不会执行任何实际比较操作。
故事的寓意是:
- 存在着将C语言源代码和CPU指令之间的转换层次,这个层次对性能有重要影响。
- 因此,仅通过查看源代码不能评估性能。
- 编译器应该足够聪明以优化这种琐碎的情况。在绝大多数情况下,程序员 不应该浪费时间考虑这些问题。
0x100000f90: jmp 0x100000f90
(地址显然会有所变化)。面试官可能在寻找一个寄存器测试或简单标记跳转之间权衡。这个问题和他们的推测都很无聊。 - WhozCraig