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

15
在大部分C或C++开发环境中,通常存在“调试”模式和“发布”模式的编译。
通过比较这两种模式,可以发现调试模式会添加调试符号(在许多编译器上通常使用-g选项),但同时会禁用大多数优化操作。
而在“发布”模式下,通常会开启各种优化操作。
那么,为什么存在这样的差异呢?
6个回答

30

没有任何优化时,代码的执行流程是线性的。如果你在第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;  // 指向内存映射I/O设备的地址
    *ptr = 0;   // 设置硬件设备
    while(*ptr == 1) {    // 循环直到硬件设备完成
       // 做一些事情
    }

关闭优化时,这很简单,您可以大致知道会发生什么。 但是,如果您打开优化,可能会发生以下几件事:

  • 编译器可能会消除while块(我们将其初始化为0,它永远不会变成1)
  • 指针访问可能被移到寄存器中,而不是访问内存->没有I/O更新
  • 内存访问可能会被缓存(不一定与编译器优化相关)

在所有这些情况下,行为都会发生显著的不同,很可能是错误的。


1
volatile int* ptr 怎么样?如果我理解正确,它将解决第2点和第3点,对吗?但是我不确定 while - Alexander Malakhov

6
另一个debug和release之间的重要区别是本地变量的存储方式。从概念上讲,局部变量在函数堆栈帧中被分配存储空间。编译器生成的符号文件告诉调试器变量在堆栈帧中的偏移量,以便调试器可以向您显示它。为了实现这一点,调试器会查看内存位置。
然而,这意味着每次更改本地变量时,源代码行的生成代码都必须将值写回到堆栈上的正确位置。由于存在内存开销,这非常低效。
在发布版本中,编译器可能会将局部变量分配给函数的一部分寄存器。在某些情况下,它可能根本不为其分配堆栈存储空间(机器的寄存器越多,这样做就越容易)。
然而,调试器不知道寄存器如何映射到代码中特定点的局部变量(我不知道任何包括此信息的符号格式),因此它无法准确地向您显示它,因为它不知道要去哪里查找。
另一个优化是函数内联。在优化构建中,编译器可能会将对foo()的调用替换为foo的实际代码,因为该函数足够小。然而,当您尝试在foo()上设置断点时,调试器想要知道foo()指令的地址,现在没有一个简单的答案 - 您的程序中可能有数千个foo()代码字节的副本。调试版本将保证有地方可以放置断点。

3
优化代码是一种自动化的过程,可以提高代码的运行时性能而不影响语义。这个过程可以删除中间结果,这些结果在表达式或函数求值时是不必要的,但在调试时可能会对您有所帮助。类似地,优化可以改变表象控制流程,以便按稍微不同的顺序执行操作,以跳过不必要或冗余的计算。这种代码重排可能会干扰源代码行号和目标代码地址之间的映射,使得调试器很难按照您编写的方式跟踪控制流。
在未经优化的模式下进行调试可以让您看到所有东西,就像您编写的那样,不会被优化程序删除或重新排序。
一旦您确信程序已经正确地工作,您就可以开启优化来获得更好的性能。即使现在的优化器非常可靠,构建一个高质量的测试套件以确保您的程序在优化和未优化模式下的功能相同仍然是一个好主意。

2
期望调试版本被 - 调试!如果每一行非空、非注释的源代码都匹配某些机器码指令,那么在调试器(IDE或其他)中设置断点、单步执行并观察变量、堆栈跟踪以及其他操作是有意义的。但大多数优化会打乱机器码的顺序,例如循环展开,公共子表达式可以从循环中提取出来。即使是最简单的优化级别,启用优化后,您可能会尝试在一个在机器码层面上不存在的行上设置断点。有时您无法监视本地变量,因为它们被保存在CPU寄存器中,甚至可能已经被优化消除了!

1

优化中的另一个问题是内联函数,因为您总是会逐个步骤地通过它们。

使用GCC,启用调试和优化后,如果您不知道该期望什么,您会认为代码行为异常,并且会多次重新执行相同语句-我有一些同事也曾遇到过这种情况。此外,带有优化的调试信息通常比它们本应具有的质量要差。

然而,在像Java这样的虚拟机托管的语言中,优化和调试可以共存 - 即使在调试过程中,JIT编译成本机代码仍将继续进行,只有经过调试的方法的代码会自动转换为非优化版本。

我想强调的是,除非使用的优化器有缺陷或者代码本身有缺陷并依赖于部分未定义的语义,否则优化不应改变代码的行为;后者在多线程编程或同时使用内联汇编时更为常见。

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

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


1

如果您在指令级别而不是源代码级别调试,则将未经优化的指令映射回源代码会更加容易。此外,编译器的优化器有时可能存在错误。

在微软的Windows部门中,所有发布的可执行文件都使用调试符号和完整的优化进行构建。这些符号存储在单独的PDB文件中,不会影响代码的性能。它们不会随产品一起发货,但大多数可在Microsoft Symbol Server上获得。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接