编译器优化及优化问题的调试方法

优化影响了那些内容

局部变量

这里提到的局部变量包含一个 block 中定义的局部变量以及参数传递中使用的局部变量。 这些局部变量可能会经历从栈里面分配到使用寄存器到直接移除的过程。这些局部变量都只是临时保存中间的计算结果,可以直接优化掉,则调试时不能看到局部变量的值。

函数中的语句的位置

写代码的时候通常会认为执行的逻辑是按照预定的排布依次执行的,可优化可能会对语句的位置进行调整,甚至将一些语句与其它的语句进行合并,这样在调试时可能会有跳前跳后的现象。

函数调用关系

优化可能会消除一些函数调用关系,编译器可能会将小函数直接展开,多个函数之间可能会合并栈帧,这样你在调试时可能会看到一个奇怪的函数名称。

优化影响调试的根本原因

调试信息是在优化之前生成的。由于优化过程中可能对程序进行了调整,这将导致调试信息与执行代码不一致,调试信息部分失效。

GCC 中一些会导致调试信息无效的情况

  1. 常规表达式的删除,如将多个实例合并为一个实例
  2. 固定不变代码的移动。如将循环中并不会改变的表达式移动到循环开始处
  3. 指令重排
    移动指令以使加载和存储与其它代码部分重叠。将计算得到的值的过程向值的使用位置靠近。赋值语句可能并不会立刻执行,当需要用到值的时候可能会跳回去执行。
  4. 大的跳跃
    不同的代码块被合并造成的结果。一些以 goto、return、break 这些跳转语句结束的序列容易产生这种现象
  5. 不固定的变量——变量的值不可预测
    赋值语句被移动,寄存器复用,值传递过程中变量被忽略等都可能造成这个问题

更详细的说明详见 Debugging Optimezed Code。

优化问题的一些解决方法

1.先检查下代码,看看有没有明显的问题

2. 关闭链接时优化(如果开启了的话)

我们在做一个项目时常常会创建多个源文件,在编译时可能会使用编译系统提供的多线程编译方式来并行的编译多个源文件。在编译每个源文件的过程中编译器会按照指定的优化方针进行优化,可这些优化都是针对单个源文件的。在这一个阶段编译器只看到了整个执行程序中的一部分,因此那些依赖程序的多个部分进行的优化便无法做到。

在链接阶段,链接器将所有的依赖文件按照实际情况加入到最终的程序中,这样的过程为那些依赖程序多个部分进行的优化提供了条件。

更多的信息详见——lto.pdf

从上面的描述中我们可以看出来链接时优化的作用范围是【非常大】的,这样的范围在大多数时间内都【超出】了我们的掌控能力,因此要调试优化问题,还是先关闭链接时优化吧!

3. 从最少的优化开始寻找问题

编译器一般都会提供多个【优化选项】,像 gcc 就提供了 O0、O1、O2、O3、OS 等优化选项。一般来说我们发布的 release 版本基本都是使用了 O3 ,这样如果出现了与 debug 不一致的表现,那么可以先调整为 O1 来调试,由于 O1 的【优化力度】更少,调试起来还是【有迹可循】的!

4. 生成中间的文件来寻找问题

一些编译器支持保留中间文件,这些中间文件可以用来分析优化的问题。gcc 中可以保留 rtl 文件,指定 –fdump-rtl-all 编译参数就会保留生成目标文件的中间文件。 查看这些文件中与优化有关的文件内容,通过对比这个文件与源代码来找到被优化掉的内容

5. 使用更严格的警告,警告中可能就蕴含着问题的解决方案

6. 注意 debug 与 release 执行时速度的差异,这个差异可能也会在一定情况中造成问题,不过多见于嵌入式开发中

7. 使用编译器提供的阻止优化的方法来阻止部分可能存在问题的函数的优化,在 gcc 中直接指定 -fxx 等参数可以关闭一类优化

解决优化问题的反面——开发中如何使用优化

上一个小节所描述的解决方案其核心是向不断减少优化力度的方向进行,而其反面就是我们在开发过程中应该使用优化的合理过程。可以看到这样的过程是从无优化到逐渐增加优化力度的过程,逐渐这样的说法表明着我们选择优化方针时应该【保守】一点。

总结

本文从优化影响的内容开始描述,慢慢逼近解决优化问题这一主题,在这样的过程中我对优化有了更深入的认识,希望能够对未来的工作有所帮助!

你可能感兴趣的:(策略,c语言)