为什么C / C ++程序经常在调试模式下关闭优化?
在大多数C或者C ++环境中,存在"调试"模式和"发行"模式编译。
查看两者之间的差异,我们会发现调试模式添加了调试符号(通常在许多编译器上使用-g选项),但同时也会禁用大多数优化。
在"发布"模式下,通常会启用各种优化。
为什么会有所不同?
解决方案
回答
如果不进行任何优化,则代码流是线性的。如果我们位于第5行并且是单步执行,那么我们将跳至第6行。启用优化后,我们可以进行指令重新排序,循环展开和各种优化。
例如:
void foo() { 1: int i; 2: for(i = 0; i < 2; ) 3: i++; 4: return;
在此示例中,如果没有优化,则可以单步执行代码并按第1、2、3、2、3、2、4行
启用优化后,我们可能会看到如下所示的执行路径:2、3、3、4甚至只是4! (该功能根本不执行任何操作...)
最重要的是,启用优化功能的调试代码可能会很痛苦!特别是如果我们具有大型功能。
请注意,启用优化会更改代码!在某些环境(安全关键系统)中,这是不可接受的,要调试的代码必须是出厂的代码。在这种情况下,必须进行调试并进行优化。
尽管优化和未优化的代码应在功能上等效,但在某些情况下,行为将发生变化。
这是一个简单的例子:
`
int* ptr = 0xdeadbeef; // some address to memory-mapped I/O device *ptr = 0; // setup hardware device while(*ptr == 1) { // loop until hardware device is done // do something }
`
禁用优化后,这很简单,我们就知道会发生什么。
但是,如果打开优化,则可能会发生两件事:
- 编译器可能会优化while块距离(我们将init设置为0,永远不会为1)
- 代替访问内存,指针访问可能会移动到寄存器->无I / O更新
- 可能会缓存内存访问(不一定与编译器优化相关)
在所有这些情况下,行为都将大为不同,并且很可能是错误的。
回答
调试和发布之间的另一个关键区别是局部变量的存储方式。从概念上讲,局部变量是在函数堆栈框架中分配存储的。编译器生成的符号文件告诉调试器堆栈框架中变量的偏移量,以便调试器可以将其显示给我们。调试器偷看内存位置以执行此操作。
但是,这意味着每次更改局部变量时,为该源代码行生成的代码都必须将该值写回到堆栈上的正确位置。由于内存开销,这是非常低效的。
在一个发行版中,编译器可以将局部变量分配给部分函数的寄存器。在某些情况下,它可能根本不为其分配堆栈存储空间(一台计算机拥有的寄存器越多,执行此操作就越容易)。
但是,调试器不知道寄存器如何映射到代码中特定点的局部变量(我不知道任何包含此信息的符号格式),因此它无法像它那样准确地向我们显示不知道去哪里找它。
另一个优化是函数内联。在优化的版本中,编译器可能会在每次使用foo()的地方都使用对foo()的实际代码替换对foo()的调用,因为该函数足够小。但是,当我们尝试在foo()上设置断点时,调试器希望知道foo()的指令地址,并且对此不再有简单的答案-可能有成千上万的foo()副本。代码字节分布在程序中。调试版本将确保我们可以在某个地方放置断点。
回答
期望对调试版本进行调试!如果每行非空,无注释的源代码都与某些机器代码指令匹配,那么设置断点,单步执行变量,堆栈跟踪以及在调试器(IDE或者其他方式)中所做的所有其他事情都是有意义的。
大多数优化都与机器代码的顺序混淆。循环展开就是一个很好的例子。常见的子表达式可以取消循环。启用优化后,即使是最简单的级别,我们也可能试图在机器代码级别不存在的行上设置断点。有时我们无法监视局部变量,因为它被保存在CPU寄存器中,甚至可能已经过优化而已!
回答
如果我们是在指令级别而不是源代码级别进行调试,那么这很麻烦,我们可以更轻松地将未优化的指令映射回源代码。另外,编译器有时在其优化器中也有漏洞。
在Microsoft的Windows部门中,所有发行版本的二进制文件都带有调试符号和完整的优化功能。这些符号存储在单独的PDB文件中,不会影响代码的性能。它们不随产品一起提供,但是大多数都可以从Microsoft Symbol Server获得。
回答
优化代码是一个自动化的过程,可以在保留语义的同时提高代码的运行时性能。此过程可以删除不需要完成表达式或者函数求值的中间结果,但是在调试时可能会对我们感兴趣。类似地,优化可以改变外观上的控制流,从而事情发生的顺序可能与源代码中出现的顺序略有不同。这样做是为了跳过不必要或者多余的计算。代码的这种重新编排可能会使源代码行号和目标代码地址之间的映射混乱,从而使调试器难以在编写代码时遵循控制流。
在未优化模式下进行调试可让我们在编写过程中看到自己编写的所有内容,而无需优化程序删除或者重新排序。
对程序正常运行感到满意后,就可以打开优化以提高性能。即使这些天优化器是值得信赖的,但构建一个高质量的测试套件以确保程序在优化和未优化模式下运行相同(从功能的角度,不考虑性能)仍然是一个好主意。
回答
优化的另一个问题是内联函数,从某种意义上说,我们总是会单步执行它们。
使用GCC,同时启用调试和优化功能,如果我们不知道要期待什么,我们将认为代码行为不当,并多次执行同一条语句,这是我几个同事遇到的。
实际上,由GCC提供的调试信息在优化上的质量往往比实际情况差。
但是,在虚拟机(如Java)托管的语言中,优化和调试甚至可以在调试期间共存,将JIT编译为本机代码仍在继续,并且只有调试方法的代码会透明地转换为未优化的版本。
我想强调的是,除非使用的优化器有错误,或者代码本身有错误并且依赖于部分未定义的语义,否则优化不应更改代码的行为。后者在多线程编程中或者在使用内联汇编时更为常见。
Code with debugging symbols are larger which may mean more cache misses, i.e. slower, which may be an issue for server software.
至少在Linux上(没有理由Windows应该有所不同),调试信息打包在二进制文件的单独部分中,并且在正常执行期间不会加载。可以将它们拆分为其他文件以进行调试。
另外,在某些编译器(包括Gcc,我猜也可能是Microsoft的C编译器)上,调试信息和优化可以同时启用。如果没有,显然代码将变慢。