Stay Hungry.Stay Foolish.
神奇的编译器优化

目的是为了可以让我更加清晰的了解C和ASM之间代码的对照,同时了解一些编译器的优化技巧,方便起见,我这里都用GCC来演示,编译统一开启O3优化,输出intel格式汇编语法gcc xxx.c -O3 -S -masm=intel -o xxx.s。

编译优化的手段
  1. 函数内嵌(inlining)
  2. 无用代码删除(Dead code elimination)
  3. 标准化循环结构(loop normalization)
  4. 循环体展开(loop unrolling)
  5. 循环体合并
  6. 分裂(loop fusion,loop fission)
  7. 数组填充(array padding)
  8. 删除无用的 NULL 检测
例子
float *P;
void zero_array(){
    int i;
    for(i = 0; i < 10000; ++i) {
        P[i] = 0.0f;
    }
}

编译之后的代码如下

zero_array:
.LFB12:
    .cfi_startproc
    mov rdi, QWORD PTR P[rip]
    mov edx, 40000
    xor esi, esi
    jmp memset
    .cfi_endproc

这段汇编其实翻译为c代码就是memset(P, 0, 40000),因为float占4个字节,可以看到编译器把一个10000次4字节的写内存操作变成了一次memset操作。

int shift(int a) {
    return a << 4; 
}

汇编之后的代码

shift:
.LFB13:
    .cfi_startproc
    mov eax, edi
    sal eax, 4
    ret
    .cfi_endproc

这里比较有意思的是如果我把4改为3,得到的汇编如下

shift:
.LFB13:
    .cfi_startproc
    lea eax, [0+rdi*8]
    ret
    .cfi_endproc

直接通过lea有效偏移地址来计算,这里左移3位相当于2^3 == 8。 继续,这回我把4改为1,下面应该是你脑子里面猜想的答案吧?

shift:
.LFB13:
    .cfi_startproc
    lea eax, [0+rdi*2]
    ret
    .cfi_endproc

其实,不是……正确的是下面这样

shift:
.LFB13:
    .cfi_startproc
    lea eax, [rdi+rdi]
    ret
    .cfi_endproc

这里使用rdi和rdi的一次加运算来代替rdi*2运算,众所周知寄存器的加运算的指令周期小于乘法运算。 继续,这回我把4改为0,机智的你应该可以想到会得到下面的汇编

shift:
.LFB13:
    .cfi_startproc
    mov eax, edi
    ret
    .cfi_endproc

这个例子里面我们可以看到仅仅是一个4字节数据的左移操作,编译器就对它划分了4种情况来进行汇编代码生成。

自由转载-非商用-非衍生-保持署名(创意共享3.0许可证
评论
2016-12-15 09:42:48

<img src=x>