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

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

其他提示

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

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

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

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

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

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

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

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

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

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

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

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

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

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

然而,在 Java 等虚拟机托管的语言中,优化和调试可以共存 - 即使在调试期间,JIT 编译为本机代码也会继续,并且只有已调试方法的代码会透明地转换为未优化的版本。

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

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

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

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top