LuckyHu Blog

首页 产品 工作室 简历

Linux程序性能优化探索(1)

09 Oct 2018

最近这段时间应该会把主要精力放在linux程序的性能优化上面,希望通过这个系列的博客有所积累。

先抛砖引玉说一下最近在做一些基础性测试的时候遇到的比较有趣的编译优化的问题,虽然是比较基础的部分,但以前并没有这么精细地去研究过相关的领域,但又模模糊糊地有一些了解,导致在测试过程中走了不少弯路。

`

int bench_plus(){
    //BENCH_START;

    int r = 0;
    for (int i = 0; i <LT ; ++i) {
        r = i + 1;
    }

    //BENCH_END("plus");

    return r;
}

`

以上这段代码在gcc -O3的编译选项下,产生的汇编代码如下(去掉了无关部分):

bench_plus: .LFB36: ​ .cfi_startproc ​ movl $1000000, %eax ​ ret ​ .cfi_endproc

可以看到整个一百万次的循环(LT为一个定义为一百万的宏),全部都被优化掉了,本意是希望测试一下加法的大致耗时,因为编译优化的原因,测试结果肯定是无效的。

以上就是模糊地懂的部分了,于是我就把编译优化选项改为了gcc -Og,这个选项是《CSAPP》这本书推荐的阅读汇编代码的最好的编译选项。同样的代码,得到的汇编为:

bench_plus: .LFB36: ​ .cfi_startproc ​ movl $0, %eax ​ jmp .L7 .L8: ​ addl $1, %eax .L7: ​ cmpl $999999, %eax ​ jle .L8 ​ rep ret ​ .cfi_endproc

可以看到这段代码同样也被适度地优化了,最后返回了放在eax寄存器的值,这个值其实是最后一次i++的值,也就是说这段汇编确实进行了循环,但循环中的加法并没有做,并且全程都没有r这个变量,非常巧妙。

但是我主要是想测试100w次加法啊,所以我猜测是这个r=i+1刚好和循环中的++i是一个意思,于是我改成了r=i+3,汇编为:

bench_plus: .LFB36: ​ .cfi_startproc ​ movl $0, %edx ​ movl $0, %eax ​ jmp .L7 .L8: ​ leal 3(%rdx), %eax ​ addl $1, %edx .L7: ​ cmpl $999999, %edx ​ jle .L8 ​ rep ret ​ .cfi_endproc

这里可以看到,循环体中的代码终于没有被优化掉了,但并不是我预期中的add命令,3(%rdx)应该是一个变址寻址的符号,但加上leal这里就变成了加载地址,于是它的意思就是把放在rdx寄存器(也就是edx)中的值+3,但是并不去寻址,而是把这个值直接加载到eax寄存器中,就完成了r=i+3这个c语句,很神奇。我猜测是不是lea命令比add命令要快。

这一小段代码的分析,主要是想表达一个意思,编译器的编译优化是相当智能的,甚至有一定的推理能力,比我之前认知的要厉害很多,尤其是在O3这个级别,即便是Og的级别的编译选项,生成的机器代码,也有很多技巧在里面,并不会完全和原c代码有那么明显的对应关系。所以接下来,我准备稍微改变一下我的探索方向,之前的计划是采用测试的方式去把一些基础的语法都测试一下,希望对每一个操作都有一个大致的了解,但引入编译优化这个问题以后,其实我们所写的代码和最终跑起来的代码应该会有很大的区别,所以我觉得直接盲目去做基础测试并没有太大意义。我决定还是先从理论上研究一下编译优化相关的东西。

在网上做了一些功课,希望能找到比较系统的方式来学习,看到有人推荐cmu的编译优化的课程,地址:https://www.cs.cmu.edu/~15745/index.html,大致翻了下感觉还不错,但是我看课程大纲,主要是从llvm入手的,这个我们实际场景使用gcc不太一样,所以让我有所顾虑,再次搜索一番,看到有人推荐直接研究gcc的编译选项对应的优化选项,我觉得这是个不错的主意,如果我能把每一种编译优化的选项都弄明白,那应该也就知道有哪些优化点应该注意了吧。

这里需要说一下,我们直接使用的编译选项O3或者O2之类的,其实是一系列编译优化选项的默认合集的意思,所以我说的,是去研究这个合集中的每一个编译优化选项,我找了一下gcc官网的说明,还好有非常详细的列表,所以接下来我会去一项一项地搞明白这些选项的意义。

这个编译优化选项的列表地址如下:

https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html