为什么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! (该function毕竟没有...)

最重要的是,启用优化的调试代码可能是一个巨大的痛苦! 特别是如果你有大function。

请注意,启用优化会更改代码! 在某些环境(安全关键系统)中,这是不可接受的,被调试的代码必须是出厂的代码。 在这种情况下,需要进行优化调试。

虽然优化和非优化代码应该在“function上”等效,但在某些情况下,行为会发生变化。
这是一个简单的例子:

 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块(我们初始化为0,它永远不会是1)
  • 指针访问可能会移动到寄存器 - >无I / O更新,而不是访问内存
  • 内存访问可能被缓存(不一定与编译器优化相关)

在所有这些情况下,行为将完全不同,并且很可能是错误的。

调试和发布之间的另一个重要区别是如何存储局部变量。 从概念上讲,局部变量在函数堆栈帧中分配存储。 编译器生成的符号文件告诉调试器堆栈帧中变量的偏移量,因此调试器可以向您显示。 调试器会查看内存位置以执行此操作。

但是,这意味着每次更改局部变量时,该源代码行的生成代码都必须将值写回堆栈上的正确位置。 由于内存开销,这是非常低效的。

在发布版本中,编译器可以将本地变量分配给寄存器以用于函数的一部分。 在某些情况下,它可能根本不为它分配堆栈存储(机器越多,寄存器就越容易)。

但是,调试器不知道寄存器如何映射到代码中特定点的局部变量(我不知道任何包含此信息的符号格式),因此它无法准确地向您显示,因为它没有不知道去哪里寻找它。

另一个优化是函数内联。 在优化的构建中,编译器可以将foo()的调用替换为foo的实际代码,因为函数足够小。 但是,当你尝试在foo()上设置断点时,调试器想要知道foo()的指令的地址,并且不再有一个简单的答案 – 可能有数千个foo的副本( )代码字节遍布您的程序。 调试版本将保证您可以在某处放置断点。

优化代码是一个自动化过程,可在保留语义的同时提高代码的运行时性能。 此过程可以删除不完整的中间结果以完成表达式或函数评估,但在调试时可能会对您感兴趣。 类似地,优化可以改变明显的控制流,以便事情可能以与源代码中出现的顺序略有不同的顺序发生。 这样做是为了跳过不必要或冗余的计算。 这种代码的重新调整可能会破坏源代码行号和目标代码地址之间的映射,这使得调试器很难在编写代码时遵循控制流。

在未优化模式下进行调试可以让您在没有优化器删除或重新排序的情况下查看您编写的所有内容。

一旦您对程序正常运行感到满意,您就可以启用优化以提高性能。 尽管优化器现在非常值得信赖,但建立一个高质量的测试套件仍然是一个好主意,以确保您的程序在优化和未优化模式下以相同的方式运行(从function的角度来看,不考虑性能)。

期望是调试版本 – 调试! 如果每行非空,非注释源代码与某些机器代码指令匹配,则设置断点,在监视变量时单步执行,堆栈跟踪以及在调试器(IDE或其他方式)中执行的所有操作都是有意义的。

大多数优化都与机器代码的顺序相混淆。 循环展开就是一个很好的例子。 常见的子表达式可以从循环中解除。 打开优化后,即使是最简单的级别,您也可能尝试在机器代码级别不存在的行上设置断点。 有时您无法监视本地变量,因为它保存在CPU寄存器中,或者甚至可能优化不存在!

如果您在指令级别而不是源级别进行调试,那么您可以更轻松地将未经优化的指令映射回源。 此外,编译器偶尔会出现错误。

在Microsoft的Windows部门中,所有发布二进制文件都使用调试符号和完全优化构建。 符号存储在单独的PDB文件中,不会影响代码的性能。 它们不随产品一起提供,但大部分都可以在Microsoft Symbol Server上获得 。

优化的另一个问题是内联函数,在某种意义上说,您将始终单步执行它们。

使用GCC,一起启用调试和优化,如果你不知道会发生什么,你会认为代码行为不端并多次重复执行相同的语句 – 这发生在我的几个同事身上。 另外,GCC给出的优化调试信息往往质量比实际上差。

但是,在像Java这样的虚拟机托管的语言中,优化和调试可以共存 – 即使在调试期间,JIT编译到本机代码也会继续,并且只有调试方法的代码才会透明地转换为未优化的版本。

我想强调优化不应该改变代码的行为,除非使用的优化器是错误的,或者代码本身是错误的并且依赖于部分未定义的语义; 后者在multithreading编程中或在使用内联汇编时更常见。

具有调试符号的代码较大,这可能意味着更多的高速缓存未命中,即更慢,这可能是服务器软件的问题。

至少在Linux上(并且没有理由说Windows应该不同)调试信息打包在二进制文件的单独部分中,并且在正常执行期间不会加载。 它们可以拆分为不同的文件以用于调试。 此外,在一些编译器(包括Gcc,我猜也是微软的C编译器)上,调试信息和优化都可以同时启用。 如果没有,显然代码会变慢。