C++编程实践——编译器优化实践

一、说明

在前面对C++程序的优化进行了细节上和全面上的分析说明,大家基本明白了如果何对程序进行优化以及优化的时机等。一般来说,开发者会更倾向于代码相关的优化。不过编译器的优化往往容易被开发者忽略,但其实编译器的优化与代码的耦合程度并不低。虽然说现代的编译器已经在某些方面变得很智能,但实际上在不少的情况下仍然需要开发者自己处理编译器相关的设置及代码的适配。
本文将对这两个方面展开相关的优化流程说明和相关建议,与大家一起讨论。

二、优化的指标

优化的指标很多,但仅对编译器来说,优化的指标主要有:

  1. 编译时间
    这个是最容易为开发者看到的。在相同的情况下,编译的时间往往能让开发者感觉到非常无力。举个前面的例子,在高配置的服务下,使用多个CPU核心编译Linux内核的话,速度有革命性的提高。当然在此处这个例子不太合适,只不过达到的效果是一样的。其实在一些大型的程序中,优化往往能显著的提高编译速度
  2. 编译后可执行文件的大小
    这也是比较容易为开发者看到的,通过合理的编译优化,可以将最终的可执行文件变得较小。其中包括死代码的删除、模板的实例优化以及调试信息等内容
  3. 编译后可执行文件的执行效率
    编译器通过优化后可以进行分支预测、减少循环甚至并行使用相关指令以及通过内联等来提高执行速度等
  4. 编译后的资源适配
    特别是内存相关的处理,包括缓存的处理、内存是否对齐以及是否需要预取等 ;同时,通过指令优化等可以动态控制资源的使用,在保证效率的同时降低相关的能耗

这些指标可能在实际的应用中会有不同的侧重点,大家可以根据实际应用的目的来确定相关的优化选项,而不要撒芝麻盐儿,哪儿都想搂一把。

三、编译器优化的方法

编译器优化的方法分为三大类:

  1. 编译器本身使用的优化
    最简单的方法是使用编译器自身提供的相关编译选项,这个也是大家经常使用,比如在MSVC中设置相关的Release的优化级别,下面看一看g++中的编译优化选项:
    -O0:不优化
    -O1/O:这两个是一样的,在保证编译速度的情况下,尽量进行算法的优化并提高执行代码的运行速度,其包括-fauto-inc-dec等编译选项
    -O2:牺牲部分编译速度,除满足上一级别的优化外,还会尽量匹配目标配置的支持优化的算法并提高目标代码的执行速度,其包括-fthread-jumps等编译选项
    -O3:除支持上一级(-O2)的优化外,还可以采用向量处理算法,提高编译后的执行代码的并行度,并合理利用CPU中的流水线及缓存等。这个风险相当大,需要非常谨慎的使用,
    而且其效果未必能达到编译的效果。其包括-finline-functions等选项
    -Os:这个选项的优化和O3类似,但它更倾向于编译后目标代码的大小而非如-O3中牺牲编译后代码体积来提高编译后目标执行速度,其包括-falign-functions等选项
    -Ofast:等同于一个扩展,其不会严格支持语言标准,除支持-O3外,会对特定的部分进行优化如-ffast-math等
    -Og:优化调试体验,即在保持一定的编译速度和良好调试体验外,增加合理的优化级别。其包括-fbranch-count-reg等选项
    在上面的优化选项中,g++还分别提供了更多的细分的优化选项,这些细节大家可以认真参看相关的文档说明。

    # 无优化,其中 --O0可以替换为-O1,O2,O3,代表优化级别从1~3(注意大写字母O)
    # 其中O1是默认,即写不写都是这个选项
    g++ -O0 -o test main.cpp
    
  2. 代码编写让编译器更易优化
    在C++开发时,有时可能需要显示的编写一些代码,这样更易于编译器展开相关的优化工作,最典型的就是使用内联函数,优化循环语句,使用移动语句和constexpr等方式,包括前面的SIMD等方式,都可以为编译器优化提供指导,看一个简单例子:

    Test(Test&& other) {
        data_ = std::move(other.data_);
    }
    
    constexpr int recursion(int num) {
        return (num <= 1) ? 1 : (num * recursion(num - 1));
    }
    
    int main() {
        constexpr int result = recursion(5);
    }
    
    

    SIMD等相关的优化的代码方法在前面分析过,此处不再赘述。有兴趣的话,资料非常多。另外尽量避免使用volatile,以防止编译不再优化相关代码;同样,对齐内存并严格控制异常的应用,都是对编译器优化很友好的。这些编写代码提供编译器优化的地方,在不同的平台和不同的编译器甚至是相同编译器的不同版本中,都有细节上的区别。
    还有,对于模板和元编程中,也要减少推导和特化的过程(如延迟加载),这样可以提高编译的速度和准确性。当然,开发者一定要注意实际的应用效果,不要犯了经验主义和教条主义。

  3. 可以使用某些选项通过汇编代码进行反向分析优化
    比如g++可以使用-s等选项生成汇编代码,从而更有针对性的对瓶颈进行相关的优化,毕竟汇编代码更倾向于硬件的执行。另外,可以使用PGO(Profile Guided Optimization),如同反汇编一样,分析相关数据,然后再根据分析来优化编译。当然,这种优化可能对开发者的要求高不少,但再高也得慢慢搞定。

四、优化问题

正如前面反复分析一样,为了优化而优化往往会引出新的问题。所以在提出优化时,一定要注意下面的问题:

  1. 不要过早优化
    过早优化没有意义,除了不一定是最终的结果外,还有可能引入不好处理的复杂度
  2. 增加调试的复杂度
    较深的优化,往往导致调试时无法准确的确定相关的问题位置。这是一个非常重要的问题,毕竟一开始还是以实现功能为主
  3. 增加维护的复杂性
    前面也提到过,过度的优化,有可能导致代码的可读性大大降低,从而导致维护者不好对项目进行维护
  4. 优化导致的可移植性降低
    一般来说,过深的优化往往会强绑定到某种硬件或平台,亦或是某种技术等。这就有可能降低了代码的可移植性
  5. 降低了可扩展性
    过深的优化,极有可能导致耦合性增加,从而极有可能使得扩展代码的可能性大幅降低

五、总结

编译器的优化和相关代码的匹配,一般对开发者的要求比较高。至少需要开发者把相关的一引技术原理整清楚,否则就无法有针对性的进行相关的优化,甚至可能产生相反的效果。不过万事总得踢开第一步,建议开发者还是需要认真学习相关的知识,这对于开发者在日后的开发中占有不小的优势。

评论 3
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值