14

在大多数 C 或 C++ 环境中,都有“调试”模式和“发布”模式编译。
查看两者之间的区别,您会发现调试模式添加了调试符号(通常是许多编译器上的 -g 选项),但它也禁用了大多数优化。
在“发布”模式下,您通常会开启各种优化。
为什么有区别?

4

6 回答 6

29

如果没有任何优化,通过代码的流程是线性的。如果您在第 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 块(我们初始化为 0,它永远不会是 1)
  • 不是访问内存,而是指针访问可能会移动到寄存器->无 I/O 更新
  • 内存访问可能被缓存(不一定与编译器优化相关)

在所有这些情况下,行为都会大不相同,而且很可能是错误的。

于 2008-09-16T03:51:20.933 回答
6

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

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

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

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

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

于 2008-09-16T04:07:18.633 回答
3

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

在未优化模式下调试允许您在编写时查看您编写的所有内容,而无需优化器删除或重新排序。

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

于 2008-09-16T05:59:59.780 回答
2

期望调试版本被调试!如果每一行非空、非注释源代码都与某些机器代码指令匹配,那么设置断点、单步执行同时观察变量、堆栈跟踪以及您在调试器(IDE 或其他方式)中所做的一切都是有意义的。

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

于 2008-09-16T04:08:01.720 回答
1

如果您在指令级别而不是源代码级别进行调试,那么将未优化的指令映射回源代码对您来说非常容易。此外,编译器的优化器有时会出现错误。

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

于 2008-09-16T05:46:58.457 回答
1

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

使用 GCC,同时启用调试和优化,如果您不知道会发生什么,您会认为代码行为不端并多次重新执行相同的语句 - 我的几个同事发生了这种情况。实际上,GCC 提供的经过优化的调试信息的质量往往比它们可能的要差。

但是,在 Java 等虚拟机托管的语言中,优化和调试可以共存——即使在调试过程中,JIT 编译为本机代码仍在继续,只有被调试方法的代码透明地转换为未优化的版本。

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

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

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

于 2009-01-11T20:58:51.090 回答