对优化代码进行调试
通过正确的权衡来获得最便捷有效的故障排除及最快速可行的优化
使一个程序变得更加容易进行调试和优化,需要内在的权衡。对源代码进行越多的优化,程序与初始代码看起来就更不相像。因此,对程序进行调试也更加困难,因为实际代码运行起来并不跟最初始的代码相一致。这篇文章描述了一种持续的权衡方法,可以在调试程序和使之运行更快之间取得正确的平衡。这篇文章同时还描述了如何调试内联函数及过程。
编写一个运行得快的程序并不容易。编译器可以帮助将一个程序转换成运行得更加快,但权衡是转换之后的程序与初始的程序会有所不同。在某些作了积极优化转换(aggressive optimization transform)的情形里,从人类的角度看转换后的程序与初始程序相比较已经几乎没有可读的一致性了。
因此,调试有问题的已优化程序会更加困难。这是一个问题,因为为了获得最大化的性能优势,大多数产品代码都运行程序的优化版本。
调试已优化程序
通常,当您尝试通过一个调试器去调试一个高度优化过的程序,与初始代码相关的许多信息已无法呈现 — 又或者即便可以显示,也不正确。通常的情形包括调试一个已被内联的函数或显示一个已被优化的变量值。这些情形经常需要人工映射汇编代码或程序的其他展现形式来与初始代码进行比较 — 这一过程很困难或很容易出错。
IBM XL C/C++ 编译器总是可以严格限制优化或为应用程序提供调试信息,来使得对程序进行调试更加简单。在跟随 z/OS V2R1 发行版发布的 z/OS XL C/C++ 编译器中,一组主要的调试器功能增强是在普通优化级别及内联机制上,这使得调试已优化的程序变得更加容易。
通过引入调试器级别,编译器可以提供关于初始程序各个方面准确和确切的信息 — 例如,参数的值和分支点上允许的断点。通过内联调试方面的增强,编译器提供了各个变量在函数或过程的内联实例中的局部数值方面的信息。
这些调试增强功能使得编程更加容易快捷,并减少维护成本;生成的应用程序不仅可以被优化,而且仍然可以被调试。
遍历代码
在源代码级别,您可以描述程序应当如何运行,这也是您最熟悉的代码级别。但为了更好地利用硬件 — 例如尝试填充一个指令缓存来减少检索代码的延迟 — 优化可能改动了代码。这样做之后,指令的顺序在源代码级别和机器实际运行的可执行代码级别上,实际上已经不同。因此,当您调试可执行代码时,您无需与源代码进行一对一的比对。
调试与优化选择
在特定的源代码行放置断点(breakpoint)并不会导致调试器停止可执行代码,在调试器中通过步进的方式执行程序也无法使之停止在下一行代码行上。保持与源代码次序一致的执行顺序会限制编译器所能执行的优化,只能得到一个运行缓慢的应用程序。
不过,有一个折中的方式。通过折衷某些级别的“源代码-可执行代码的直接映射”(source-code-to-execution-code direct mapping),编译器可以优化直接映射的源代码行之间对应的执行点,并保持在重要的源代码行上的映射,这样来允许在那些执行点上进行调试。
调试级别支持与信息
调试级别支持(debug level support)提供了调试级别的范围,或者关于调试器所提供调试信息正确性的最小范围的约定。例如,在高级别,级别 9,所有可执行语句都可以在调试器中停止,所有变量的值都可以被改变并使改变反映在运行程序的后续执行中。另一方面,调试级别 1 则只提供行号表信息,并不保证也不约定可以在任意特定源代码行停止的能力。
其他级别在这两种极限之间的标志性代码事件上提供停止点,例如分支点(if
语句、函数调用、循环入口),可以提供调试大部分应用程序所需要的主要信息。此外,对于查看变量值的常见情形,范围则从完全不保证能看到准确值的情形变化到可以看到函数参数及所有变量值的改变。
您可以从表 1 的总结看到所有重要的调试级别,以及这些级别所提供的相关信息:
表 1:调试级别影响范围
调试级别(Debug level) | 启用断点(Breakpoint)的源代码行 | 变量影响(Variable effect) |
---|---|---|
1 | - 生成无保证的行号表,行号只与未进行优化的程序源代码行相关 | - 无变量信息 |
2 | - 生成无保证的行号表,行号只与已进行优化的程序源代码行相关(与调试器级别 1 相同) | - 变量的信息已被生成,但不保证正确性 |
3 | - 生成无保证的行号表,行号只与已进行优化的程序源代码行相关(与调试器级别 1 相同) | - 函数参数在专门提供给 KPLINK 的内存中可见 |
5 | - 有 if 语句、函数调用、循环以及函数的第一行可执行语句都可以设定停点 - 行号表只列出有 if 语句、函数调用、循环以及函数的第一行可执行语句所在的行 |
- 变量只在源代码行的列中所给出的点上可见,而且仅在这些点上是正确的 |
8 | - 每一句可执行语句 - 行号表只列出每一个可执行语句行 | - 变量只在源代码行列中给出的点上是正确的 |
9 | - 每一句可执行语句 - 行号表只列出每一个可执行语句行 | - 变量只在源代码行列中给出的点上是正确的且可以修改 |
注解: 没有在表 1 中列出的调试器级别其表现与表中已列出的上一级相同 — 例如,级别 4 提供了与级别 3 完全相同的信息。这些未列出的级别在未来的版本中或许会有不同的功能性。
调试内联函数
当编译器优化程序时,它通常内联函数或过程来减少一个显式函数调用的开销。这些内联过程包括例如前言(prolog)和结尾(epilog)代码,它们是除函数代码本身的以外需要运行的代码。内联函数通常拥有它们独有的函数参数及局部变量,如果函数尚未内联的话,可以通过直接关联初始代码来进行调试。在内联了函数之后,通常二义性(例如变量在调用的源函数中拥有同名变量)会使调试更加困难或几乎不可能,因为这样做的话,无法再对这一函数调用设置断点。
内联函数或过程的方式
一个函数或过程可以用多种方式内联。方式包括使用内联选项(本身便允许进行较广范围的调优)、优化选项、C 和 C++ 中的 inline
关键字,又或者通过使用内联 #pragmas。如果函数通过这些方式中的任何一种内联,调试这些函数就会出现问题。这是因为函数本身可能并不存在,又或者函数存在但被执行的函数无“实例”(因为它被内联了)。
链接效应(Linkage effects)
此外,XPLINK 链接规范允许参数可以传递到寄存器中,而不是传递到传统多重虚拟存储(Multiple Virtual Storage,MVS)链接对应的内存里。这意味着期望看到和修改函数参数的调试器会去修改内存位置里的参数(如果 XPLINK STOREARGS 子选项被指定了)。然而这一动作对程序本身毫无作用,因为周边代码使用的参数是寄存器副本。
采用调试级别支持
使用调试级别支持的级别 2 及以上级别,您可以调试内联函数参数及局部变量数据,而无需考虑函数所使用的内联方式。例如,使用调试级别 8,您可以查看内联函数每一个语句的局部变量。在级别 9,您设置可以改变变量的值,并影响程序。函数参数可以被修改,而且改变会反映到正在运行的程序中。
内联函数的常见例子包括面向对象编程常用的 getter 及 setter 方法。这些函数,一旦内联,在足够高的调试级别上可以使它们的参数被调试器在运行时改变。例如,setter 函数可以改变参数值来允许设置一个不同的值,不同于初始源代码所描述的值。
指定和使用调试器级别
在本文中所描述的调试器级别是 z/OS V2.1 XL C/C++ 编译器中的新引入的,所以当前对这些编译器级别的需求还很少。虽然,新的调试器级别特性避免了与已有构建版本的兼容性问题。
在 Unix® System Services(USS)中已有的 –g 选项,没有改变其行为。不建议使用的 TEST 选项仍然提供 ISD 调试信息,并且,因为是不建议使用选项,并没有在这次的新级别支持中得到增强。DEBUG 选项则通过新调试器级别得到了增强,并作为 LEVEL 子选项的新值。已有的默认值并没有被改变,默认的 LEVEL 子选项的含义也没有改变。调试器级别 0 仍然表示其之前的含义,并且对于非优化编译来说与新的级别 9 等同,对于优化编译来说等同于级别 2。
简化调试规范
如果是在 USS 中使用调试器级别,已经添加了新的标志(flag)来使调试规范变得更简单。–g 标志现在可以在它的后边用可选的数字来代表调试器级别。
注解: –g flag 新的数字版本允许优化使用相应的调试特性,而 –g 标志 没有 数字跟随其后的话,则沿用旧的行为,即强制进行没有优化来允许完全的调试特性。例如,表 2 显示了等同的命令行,来启用在每一个可执行语句实现停止及查看(并不需要改变)变量的值:
表 2:等同的调试器级别规范
xlc –qdebug=level=8 –O2 hello.c |
xlc –Wc,"DEBUG(LEVEL(8))" –O2 hello.c |
xlc –g8 –O2 hello.c |
无需新的库及数据集
调试器级别不需要任何新的库或数据集被包括在构建中或用于运行程序。当然,调试器必须能够解析编译器所给的调试数据,但由于所使用的调试信息格式是使用开放的 DWARF 格式,这一信息很容易解读。
对于不同类型的程序、编程组成和优化级别来说,通过更高的调试信息级别来进行性能权衡得到的结果不尽相同,因此并不存在哪一个调试器级别是最好的万能规则。通过使用这一更大范围的调试级别,您现在比以前拥有更多的选择来进行权衡,因此您可以调整优化及调试器级别来获得您程序的最佳结果。可以不断地试验直到您找到最佳的组合为止。
结论
调试一个程序和使它运行得更快之间通常是冲突的。通常只能选择其中之一:要么是一个可以调试但运行速度不理想的程序,要么是一个运行速度很快但在调试方面非常受限而且很难与初始源代码相关联的程序。
通过采用不同级别的调试信息生成过程和可执行代码修改过程来实现与初始源代码的紧密联系,使得一组在“速度快但难以调试的程序”和“速度慢但易于调试的程序”之间的选择范围得以实现。基于分支和变量值查询的首要调试领域已经相比以前提供了更高优化级别,因此您可以实现既可以易于调试又可以快速运行的产品代码。
调试内联函数的能力同样为调试已优化过程序的可能性带来了显著的提升。这一能力在面向对象的 C++ 代码中非常有益。内联函数可以使它们的参数被显示甚至被修改,而采用足够高的调试级别,局部变量同样也可以被调试。
新的调试权衡带来了新的子选项值。因此,您可以根据需要简单通过重编译来加入新的调试支持,而先前的构建版本和程序可以延续旧的模样。
原文链接:http://www.ibm.com/developerworks/cn/rational/debugging-optimized-code/