咨询服务热线:400-123-4567
发布日期:2024-05-06 05:48:40阅读:次
一段代码想要最终被计算机执行,首先需要被翻译成机器可识别和执行的指令,代码编译的过程往往包含几个步骤:
在这个过程中,
(1)、(2)依赖于上层的编程语言设计。
(3)会将分析结果编译成中间代码。在这个阶段,编译器会尝试对中间代码进行优化,通过减少无效或冗余的代码、计算强度优化等手段,以助于减少最终生成的指令数,或使用更高效的指令。
(4)基于中间代码生成机器可执行的目标代码,这个过程和操作系统、指令集、内存等相关。其中,不同的指令集也会带来不同的效率。
在这个过程中,有两个重要的角色, 和 。
其中, 为了提升代码最终的执行效率,会进行一个非常重要的步骤,就是。
以 为例,我们来简单了解一下编译优化。
编译器会在编译速度、生成代码大小和生成代码执行速度三个方面尝试进行优化。
在默认情况下, 编译器不开启编译优化,因为编译器的目标是减少编译时间、保证编译结果能够按照期望进行测试。
总体来看,这样描述还是很抽象,我们来看一个具体些的例子。
这是一段 C 语言代码,代码做了变量声明和打印。
先来看看不开启编译优化的情况下,编译出来的汇编语言结果:
( 通过 ,我们可以获得添加了变量备注的汇编结果 )
从中可以粗略看到各个变量的创建,通过 进行了 的操作,并最终通过 调用 方法。
如果我们开启编译优化呢,则结果会有很大的不同。
最直观的感受是指令少了更多,更加精简了,细看之下,会发现有一些改变:
里面一些声明了,但是没有使用到的变量,最终被移除了。 比如、、。
在代码中,变量是参与运算,但是在最终的指令中也被移除了,而这个过程被提前进行了计算,,那么 可以提前预知判断为,则在编译阶段直接进行了替换。
在指令上,实际是将:
变为了:
精简了编译出的指令结果。
在编译优化前的指令里,函数执行的最后可以看到这样一段指令:
它的目的只将累加器归 0 ,而在开启编译后话后,变成了这样:
为什么会有这样的变化呢?我们可以简单对比一下这两个命令。
我们使用GNU 汇编器,分别对这两段指令进行编译,并通过 来看看两个命令编译后的结果:
可以发现, 指令占用大小是 的 2.5 倍,从大小上来看,更胜一筹。
可以看出,编译优化会对指令进行优化,选择成本代价更低的指令,生成代码的大小也是编译优化中考虑的一部分。
从上面的例子可以看到,编译器会从不同的角度进行指令优化。
在 gcc 的编译优化选项中,我们选取其中的一部分来看看:
以为例,该优化项会尝试将非函数进行内联,对下面的代码进行优化:
代码读取main函数参数的两个参数,转化为整数猴进行想加,来看看它优化前的中间代码:
从函数中可以看到,先调用了进行加法,再调用输出。
当我们开启编译优化,并开启函数内联优化,再来看看编译结果:
通过 和 开启函数内联优化进行编译,结果发生了变化:
(1)函数的指令得到精简和优化,原因是参数实际上是开启了级优化,该级别优化包含,而则是关闭函数内联,对比两者编译下的结果就会发现一个做了内联,一个没做。
(2)函数中已经没有了,而是将函数进行内联处理,将指令放在了函数中
在上面的优化中,我们有说到一个概念,编译优化等级。gcc 将编译优化项进行了分类,划分了出多个优化等级用于不同的场景。
优化等级 | 说明 |
---|---|
-O0 | 默认优化等级,即不开启编译优化,只尝试减少编译时间 |
-O/-O1 | 尝试减少代码大小,缩短代码执行时间,不会执行需要消耗大量编译时间的优化。对于大函数的编译优化会占用更多的时间和内存。 |
-O2 | 与-O1类似,会提升编译时间和代码性能,几乎开启除了时空权衡优化外的所有优化项 |
-O3 | 在-O2级别的基础上,开启了更多的优化项,最高优化等级,以编译时间、代码大小、内存为代价获取更高的性能。在部分情况下可能会降低性能。 |
-Os | 优化代码大小,会开启-O2中不会增加代码大小的优化项 |
-Ofast | 开启-O3所有优化项,该优化等级不会严格遵循语言标准 |
编译优化对最终的代码性能有什么影响呢,来看一个例子:
依旧是一个循环加的例子,并记录加法运算时间。在不开启编译优化的情况下,执行时间如下:
开启级优化后,执行时间如下:
优化后,执行时间只有原来的10%左右,效果明显。
(但是这个例子算是一个特殊的例子,计算过程可以用上向量化计算指令,实现并行计算,才能达到这么明显的效果,其他场景下不一定,之后有机会简单介绍一下指令集)
编译器优化这么厉害,当然也有“副作用”,来看看下面的例子:
函数中的里,依赖于来判断是否结束。正常判断下,循环加溢出后变成负数,就会终止循环,结果如下:
但是当我们开启优化后,会进入死循环:
我们来看看优化前后的汇编指令:
在编译优化后,变成了死循环。
这是一个特殊的现象,在这个过程中,有符号整数越界(Signed Integer Overflow),在 C99 标准中是一个未定义行为,该行为对应的实现可能是隐式转换或者异常等。在 gcc 中,编译器会认为 overflow 不会发生,将这段代码编译为死循环。
(可能还会有其他类似的情况,具体可以参考CERT - Dangerous Optimizations and the Loss of Causality)
既然危险系数这么高,就放弃不用吗,当然不行,我们可以选择对部分代码进行编译优化:
该语句会对其之后的代码开启对应级别的编译变化,语句之前的代码不影响,举个栗子:
在编译时不开启编译优化,得到编译后的汇编指令:
此时在函数阶段是正常编译的,在之后的函数将会开启编译优化,编译成死循环。
总体来看,编译优化在部分场景下是可以获得收益的,但是也存在风险。 没有默认开启编译优化,只做了基础的编译时间优化,而其他类型编译器却又不同的选择,例如官方提供的编译器是进行编译优化的,如果能正确掌握编译优化使用的方法和场景,是能够带来意料之外的收益。