代码之家  ›  专栏  ›  技术社区  ›  Benoit

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

  •  12
  • Benoit  · 技术社区  · 16 年前

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

    6 回复  |  直到 15 年前
        1
  •  28
  •   Benoit    15 年前

    在没有任何优化的情况下,通过代码的流是线性的。如果您在第5行和单步上,请跳到第6行。打开优化后,可以获得指令重新排序、循环展开和各种优化。
    例如:

    
    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!(该函数终究不起作用…)

    底线是,调试代码与优化启用可以是皇家痛苦!尤其是当你有大功能的时候。

    注意,打开优化会更改代码!在某些环境(安全关键系统)中,这是不可接受的,并且要调试的代码必须是已发送的代码。在这种情况下,必须通过优化进行调试。

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

        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更新
    • 内存访问可能被缓存(不一定与编译器优化相关)

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

        2
  •  4
  •   Rob Walker    16 年前

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

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

    在版本构建中,编译器可以为函数的一部分分配一个局部变量给寄存器。在某些情况下,它可能根本不会为它分配堆栈存储(一台机器的寄存器越多,这就越容易完成)。

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

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

        3
  •  3
  •   mxg    16 年前

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

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

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

        4
  •  2
  •   DarenW    16 年前

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

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

        5
  •  1
  •   George V. Reilly    16 年前

    如果您是在指令级而不是源级进行调试,那么将未优化的指令映射回源级就非常容易了。此外,编译器在优化器中偶尔会出现问题。

    在微软的Windows部门,所有发布的二进制文件都是用调试符号和完全优化构建的。这些符号存储在单独的PDB文件中,不会影响代码的性能。它们不与产品一起装运,但大多数在 Microsoft Symbol Server .

        6
  •  1
  •   Blaisorblade    16 年前

    优化的另一个问题是内联函数,也就是说,您总是单步执行它们。

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

    然而,在像Java这样的虚拟机承载的语言中,优化和调试可以共存——即使在调试期间,JIT编译到本机代码仍在继续,并且只有调试方法的代码被透明地转换为未优化的版本。

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

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

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