查看两者之间的差异,您会发现调试模式添加了调试符号(通常在许多编译器上使用-g选项),但同时也会禁"/>

为什么C / C ++程序经常在调试模式下关闭优化?

为什么C / C ++程序经常在调试模式下关闭优化?

Why does a C/C++ program often have optimization turned off in debug mode?

在大多数C或C ++环境中,存在"调试"模式和"发行"模式编译。
查看两者之间的差异,您会发现调试模式添加了调试符号(通常在许多编译器上使用-g选项),但同时也会禁用大多数优化。
在"发布"模式下,通常会启用各种优化。
为什么会有所不同?


如果不进行任何优化,则代码流是线性的。如果您位于第5行并且是单步执行,那么您将跳至第6行。启用优化后,您可以进行指令重新排序,循环展开以及各种优化。
例如:

1
2
3
4
5
void foo() {
1:  int i;
2:  for(i = 0; i &lt 2; )
3:    i++;
4:  return;

在此示例中,如果没有优化,则可以单步执行代码并按第1、2、3、2、3、2、4行

启用优化后,您可能会获得如下所示的执行路径:2、3、3、4甚至只是4! (该功能根本不执行任何操作...)

最重要的是,启用优化功能的调试代码可能会很痛苦!特别是如果您具有大型功能。

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

尽管优化和未优化的代码应在功能上等效,但在某些情况下,行为将发生变化。
这是一个简单的例子:

禁用优化后,这很简单,您就知道会发生什么。
但是,如果打开优化,则可能会发生两件事:

  • 编译器可能会优化while块距离(我们将其初始化为0,永远不会为1)
  • 代替访问内存,指针访问可能会移动到寄存器->无I / O更新
  • 可能会缓存内存访问(不一定与编译器优化相关)

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


调试和发布之间的另一个关键区别是局部变量的存储方式。从概念上讲,局部变量是在函数堆栈框架中分配存储的。编译器生成的符号文件告诉调试器堆栈框架中变量的偏移量,以便调试器可以将其显示给您。调试器偷看内存位置以执行此操作。

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

在一个发行版中,编译器可以将局部变量分配给部分函数的寄存器。在某些情况下,它可能根本不为其分配堆栈存储空间(一台计算机拥有的寄存器越多,执行此操作就越容易)。

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

另一个优化是函数内联。在优化的版本中,编译器可能会在每次使用foo()的地方将其替换为foo()的实际代码,因为该函数足够小。但是,当您尝试在foo()上设置断点时,调试器希望知道foo()的指令地址,并且对此不再有简单的答案-可能有成千上万的foo()副本。 )代码字节分布在您的程序中。调试版本将确保您可以在某个地方放置断点。


优化代码是一个自动化的过程,可以在保留语义的同时提高代码的运行时性能。此过程可以删除中间结果,这些中间结果对于完成表达式或函数求值是不必要的,但是在调试时可能会引起您的兴趣。类似地,优化可以改变外观上的控制流,从而事情发生的顺序可能与源代码中出现的顺序略有不同。这样做是为了跳过不必要或多余的计算。代码的这种重新编排可能会使源代码行号和目标代码地址之间的映射混乱,从而使调试器难以在编写代码时遵循控制流。

在未优化模式下进行调试使您可以在编写过程中看到自己编写的所有内容,而无需优化程序删除或重新排列内容。

对程序正常运行感到满意后,就可以打开优化以提高性能。即使这些天优化器是值得信赖的,但构建一个高质量的测试套件以确保您的程序在优化和未优化模式下运行相同(从功能的角度,不考虑性能)仍然是一个好主意。


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

大多数优化都与机器代码的顺序混淆。循环展开就是一个很好的例子。常见的子表达式可以取消循环。启用优化(即使是最简单的级别),您可能正在尝试在机器代码级别不存在的行上设置断点。有时您无法监视局部变量,因为它被保存在CPU寄存器中,甚至可能已经过优化而已!


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

使用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编译器)上,调试信息和优化可以同时启用。如果没有,显然代码将变慢。


如果您是在指令级别而不是源代码级别进行调试,那么这很麻烦,因为您可以更轻松地将未优化的指令映射回源代码。同样,编译器有时在优化器中也有漏洞。

在Microsoft的Windows部门中,所有发行二进制文件都带有调试符号和完整的优化功能。这些符号存储在单独的PDB文件中,不会影响代码的性能。它们不随产品一起提供,但是大多数都可以从Microsoft Symbol Server获得。


推荐阅读