我写了这个简单的C程序:
int main() {
int i;
int count = 0;
for(i = 0; i < 2000000000; i++){
count = count + 1;
}
}
我想看看gcc编译器如何优化这个循环(显然添加 1 2000000000次应该是“add 2000000000 一次”)。所以:
gcc test.c 然后time
上的a.out
给出了:
real 0m7.717s
user 0m7.710s
sys 0m0.000s
$ gcc -O2 test.c 然后time on
a.out`给出:
real 0m0.003s
user 0m0.000s
sys 0m0.000s
然后我用gcc -S
反汇编了两个。第一个似乎很清楚:
.file "test.c"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
movq %rsp, %rbp
.cfi_offset 6, -16
.cfi_def_cfa_register 6
movl $0, -8(%rbp)
movl $0, -4(%rbp)
jmp .L2
.L3:
addl $1, -8(%rbp)
addl $1, -4(%rbp)
.L2:
cmpl $1999999999, -4(%rbp)
jle .L3
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (Ubuntu/Linaro 4.5.2-8ubuntu4) 4.5.2"
.section .note.GNU-stack,"",@progbits
L3添加,L2将-4(%rbp)
与1999999999
进行比较,并在i < 2000000000
时循环到L3。
现在优化的一个:
.file "test.c"
.text
.p2align 4,,15
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
rep
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (Ubuntu/Linaro 4.5.2-8ubuntu4) 4.5.2"
.section .note.GNU-stack,"",@progbits
我根本无法理解那里发生了什么!我对装配知之甚少,但我期待像
这样的东西addl $2000000000, -8(%rbp)
我甚至尝试使用 gcc -c -g -Wa,-a,-ad -O2 test.c 来查看C代码及其转换为的程序集,但结果是没有更清楚的是前一个。
有人可以简单解释一下:
答案 0 :(得分:73)
编译器甚至比这更聪明。 :)
实际上,它意识到你没有使用循环的结果。所以它完全取出了整个循环!
更好的测试是打印结果:
#include <stdio.h>
int main(void) {
int i; int count = 0;
for(i = 0; i < 2000000000; i++){
count = count + 1;
}
// Print result to prevent Dead Code Elimination
printf("%d\n", count);
}
编辑:我添加了必需的#include <stdio.h>
; MSVC程序集列表对应于没有#include
的版本,但它应该是相同的。
此刻我没有GCC在我面前,因为我已经启动进入Windows。但这是在MSVC上使用printf()
的版本的反汇编:
编辑:我的汇编输出错误了。这是正确的。
; 57 : int main(){
$LN8:
sub rsp, 40 ; 00000028H
; 58 :
; 59 :
; 60 : int i; int count = 0;
; 61 : for(i = 0; i < 2000000000; i++){
; 62 : count = count + 1;
; 63 : }
; 64 :
; 65 : // Print result to prevent Dead Code Elimination
; 66 : printf("%d\n",count);
lea rcx, OFFSET FLAT:??_C@_03PMGGPEJJ@?$CFd?6?$AA@
mov edx, 2000000000 ; 77359400H
call QWORD PTR __imp_printf
; 67 :
; 68 :
; 69 :
; 70 :
; 71 : return 0;
xor eax, eax
; 72 : }
add rsp, 40 ; 00000028H
ret 0
是的,Visual Studio进行了这种优化。我认为GCC也可能会这样做。
是的,GCC执行类似的优化。这是使用gcc -S -O2 test.c
(gcc 4.5.2,Ubuntu 11.10,x86)的同一程序的汇编列表:
.file "test.c"
.section .rodata.str1.1,"aMS",@progbits,1
.LC0:
.string "%d\n"
.text
.p2align 4,,15
.globl main
.type main, @function
main:
pushl %ebp
movl %esp, %ebp
andl $-16, %esp
subl $16, %esp
movl $2000000000, 8(%esp)
movl $.LC0, 4(%esp)
movl $1, (%esp)
call __printf_chk
leave
ret
.size main, .-main
.ident "GCC: (Ubuntu/Linaro 4.5.2-8ubuntu4) 4.5.2"
.section .note.GNU-stack,"",@progbits
答案 1 :(得分:1)
编译器可以使用一些工具来提高代码效率或更高效率&#34;:
如果从未使用计算结果,则可以省略执行计算的代码(如果计算对volatile
值起作用,则必须仍然读取这些值,但读取结果可能会被忽略)。如果没有使用提供它的计算结果,那么也可以省略执行这些计算的代码。如果这种省略使得条件分支上的两个路径的代码相同,则该条件可以被认为是未使用的并且被省略。这对任何没有超出内存访问权限的程序的行为(执行时间除外)都没有影响,或者调用附件L所称的内容&#34;关键未定义行为&#34;。< / p>
如果编译器确定计算值的机器代码只能生成某个范围内的结果,则可以省略任何条件测试,其结果可以在此基础上预测。如上所述,这不会影响执行时间以外的行为,除非代码调用&#34;关键未定义行为&#34;。
如果编译器确定某些输入将使用所写的代码调用任何形式的未定义行为,则标准将允许编译器省略任何仅在收到此类输入时才相关的代码,即使执行平台的自然行为,如果这样的输入是良性的,编译器的重写会使它变得危险。
好的编译器会做#1和#2。然而,出于某种原因,#3已成为时尚。