当前位置:首页 >> 编程开发 >> Visual C++ >> 内容

使用IBM XL CC++和XL Fortran编译器调试经优化的代码

时间:2015/5/19 21:32:17 作者:平凡之路 来源:xuhantao.com 浏览:

软件开发者们在开发产品级代码时常会面对一个艰难的选择,你总是希望你的代码性能优越,这意味着你 需要在高优化级别上编译它;同时,你可能希望调试你加入产品中的这份二进制代码,而不是编译时没有经过 优化的源文件。如果你尝试过调试优化过的代码,你可能已经知道这其中的难处了:

源代码语句不按顺序执行,或者在你希望它们执行的时候它们没有;

变量没有按预期地进行更新;

变量没有定义的值,甚至没有一个定义的标识;

在调试器内对变量的更新对程序执行不起作用 。

这不是因为编译器出了什么差错,它设计的初衷就是为了保留你程序的结果和外部行为,而不是它 在调试器中的瞬态和内部行为。

新一代编译器

在2012年年中IBM发布了IBMPower系统上C/C++和 Fortran编译器的最新版本:XLC/C++编译器V12.1和XLFortran编译器V14.1,均对应AIX和PowerLinux平台。这 些新的编译器提供了一系列数值的选择以供调试优化过的代码,你可以自由选择在完全优化代码和完全可调试 代码之间的权衡级别。在大多数其它编译器中,开发者有两个可用的选择:

编译一个没有优化的调试版本 (例如使用-g),或

一个优化过的版本,但是可调试性较差(例如使用-O2)。

在新的XLC++ V12.1和XL FortranV14.1编译器中,你可以使用一系列-g的值,从-g0一直到-g9,调试级别越低,在调试期间 观测的错误可能性越大,这意味着你可以自由权衡可调试性和性能。在程序优化时编译器会保障各级别的可调 试性,并在调试过程中在保证预期行为的情况下有效地将应用的性能最大化。

执行顺序

编译器 在优化过程中经常会在不改变程序结果的前提下重新编排逻辑,编译器这么做可能是为了直接提高程序的性能 ,或者是为了让接下来的优化能提高程序的性能。此外,为了提高处理器吞吐量或是其它的原因,当主优化阶 段完成、指令生成时,源代码行相关的指令序列可能也会被重新排序,

这些编译器的优化有两个主要 的影响:第一,如果你按源代码的行数单步跟踪程序,那么程序可能不会按照正确的顺序执行。第二,如果你 在一个过程的开始设了个断点,没有人能保证这过程的参数在这时会含有正确的值,程序甚至根本不会进入这 个过程。这是因为编译器在优化过程中将被调用的过程代码内联至调用处。在XLC/C++和 XLFortran的早先版 本中,旧的-g行为会生成全部调试信息,但它对于一个优化过的程序是否有用就不得而知了,因此,在用-g和 -O编译的程序中,当你试图去调试并观测某个特定过程被调用时的参数是什么时,你甚至不能确定过程入口处 设立的断点是否可以让你看到这些参数的实际值。然而,-g3或更高的调试级别可以保证在过程的入口处参数 是有效且可见的。

优化代码中的某些语句可能并没有被执行到,而另一些可能比在它们之前的语句更先被 执行。如果你在一个给定的源代码行中设立一个断点,你不能确定逻辑上在这之前的行中变量被赋的值是否正 确。例如,考虑一下程序段:

10 x=x+1;

11fl=fl1/fl2;

12 y=y+1;

如果我们在第11行设立一个断点并运行程序,调试器会在与第11行关联的第一条指令处 停下,但此时第10行可能还没有被执行过,因为第10行与第11行是无关的,编译器可以自由将它们调换顺序。 如果你想要单步执行这段代码,你可能会发现我们在停在第10行前就已经执行到了11行,这是一个很强的信号 (但不是一定的),说明与第10行相关的指令都还没有被执行到。 在这个例子中编译器会将除法尽早做好, 因为除法指令有很长的延迟,而此时一个先进的流水线式处理器可以在除法进行时继续工作,这样的话没有关 联上的行为,例如将x的值读入寄存器并将它加1,可能会同时进行。假设我们对在第11行处的除法结果有兴趣 。在没有优化过的程序中,我们只需要单步到第12行,再查看fl的值。但在优化过的程序中,单步到第12步不 能保证所有与第11行相关的操作都已经完成,我们可能已经完成了除法但还没有将结果存回内存,因此,确定 变量已经可以显示正确结果的唯一方法是单步执行机器指令直到你看到除法运算的结果被写回了内存空间。如 果你只关心结果的值(而不是验证是否更新了变量),取而代之你可以在除法运算更新了其目标寄存器的同时 打印它的值。

当你用 XLC/C++ V12.1或 XL FortranV14.1的-g8或更高的选项编译程序时,调试器可以 得到每个可执行语句开始时程序的状态。在先前的例子上使用-g8可以确保当你到达第11行的断点时,第10行 的加法已经完成,而且你可以通过调试器来得到其结果。

被移除的变量

调试器总是认为变量在 内存中,因此当你在调试器中检查变量时,它会打印出程序分配给该变量的内存位置中的内容。然而,编译器 在优化过程中总是尽可能在它认为安全的情况下避免更新与变量相关联的内存地址,这么设计是为了提高性能 ,因为执行读取与存储的代价非常大。如果编译器可以在变量被修改后将它保存在寄存器中,那么它就不会把 它立即写回内存里,甚至永远都不会写。这么做的副作用是相当于在调试器面前将变量隐藏了起来。考虑一个 标准的索引变量ii,请看如下例子:

20 for (ii=0; ii<10; i++)

21  sum=sum+array[ii];        在这个例子中,大多数编译器在编译时会将 ii和sum保存在寄存器里。如果在循环完成后要使用ii的话,编译器会再生成代码来将ii写回到内存位置,但 大多数情况下,如果寄存器中存放的值再也没用了的话就会被舍弃(覆写)。这可能会使得调试过程变得复杂 :

调试器辨认不出变量。当你试图打印、查看ii时,调试器不知道它是什么;

调试器只能提供ii的 类型和上下文信息,但不能显示它的值;

调试器可以显示赋给ii的内存位置的值,但这个值已经过期 或无用了。

同样,新的-g8或-g9选项可以解决这些与被移除的变量或未知的变量值相关的问题。

改变变量的值

在程序运行时用调试器改变变量的值来改变程序的行为是另一种调试应用的技术 。显然,如果编译器将变量的引用优化掉或是使得调试器找不到变量在机器级的位置,那么这技术也就没法使 用了,甚至更糟的情况是它貌似有用,但是却没有得到预期的效果。

这是-g8与-g9之间的主要区别。对于 -g8,编译器提供了程序在每条可执行语句开始处的状态,但不保证编程人员可以在调试时修改变量的值。另 一方便,-g9使得你可以修改程序变量并让此修改影响程序的执行。显而易见,编译器在生成代码时必须保证 各程序变量能在调试时被修改,因此在上述循环的例子中,在每个循环的迭代里ii都应该能从内存中被读取到 。这会大幅度地影响性能,因此只有在你觉得上述特质对你维护代码至关重要时才应该在产品级的代码上使用 -g9。在绝大多数情况下,检测程序时你完全不需要这么做就可以理解程序的行为并找出潜在的漏洞,这时, 我们更推荐使用-g8。

内联和经优化的调试

编译器在优化时经常将被调用的过程“内联”至调 用的地方,换句话说,他们用被调用的过程体的拷贝来取代调用操作。这么做是为了消除调用的代价,同时, 那些依赖于调用处上下文的优化可以作用到内联的代码上。然而,内联会使得调试变得更加困难。

当 一个过程被内联后,不再存在调用指令。大多数调试器都提供“Step”命令和“next”命令,这两者在面对语 句时行为是一致的,但在面对调用时不同,“Step”进入被调用的过程的第一条语句,而“Next”会跨过(执 行完)调用(除非在执行被调用过程时遇到了断点)。 由于当过程内联时不再有调用指令,Step和Next命令 对于一个内联过程的“调用”具有相同的效果,如果没有带扩展功能的调试器支持的话,两条命令都会执行内 联函数的代码,就好像用户正步入调用一样。

在XL编译器中,高调试级别在处理内联上相较于早先的产品 有了巨大的进步。在-g8或更高的级别中,编译器会在每个内联代码的语句中插入状态信息,使得你可以在调 试一个内联过程的调用时单步执行被调用过程的各语句、查看到正确的变量值(就算它依赖于正确的作用域信 息)、甚至在内联过程中改变本地变量的值(-g9)。然而,“Next”命令不会跨过内联过程,而是像“Step ”那样步入内联过程的代码。

要注意的是,当你单步运行内联函数中的语句且当调试器可以跟踪到运行程 序的内联过程时,该过程不会出现在调试器的调用栈上。

调试选项

请记住,当你提高经优化的调试级别,你增加了可调试性,但潜在地降低了编译完后程序 的性能。选择一个符合你调试需求的调试级别是非常重要的,你需要最小化调试对性能的影响。在很多情况下 ,相较于未做优化的二进制代码,即使在高调试级别下编译器还是可以将性能的大幅度增强。例如,一个用- O2-g8编译的典型程序的运行速度是-O2编译出的相同程序的80%,对于当需要可调试性时只用-g编译的传统方 法来说,这是一个实质性的进步。以下是对各个-g级别效果的简单说明:

-g0:没有调试信息。这是缺 省设置。

-g1:生成只含有行号和源文件名字的调试信息,没有符号表信息。

-g2:“传统的” -g行为。在优化时,编译器生成所有调试数据,但是不保证其可用性和准确性。所有之前提到过的问题都有可 能发生。

-g3,-g4:类似-g2,另外,调试器可以在过程开始处查看到函数的参数值。

-g5,- g6,-g7:类似-g3,另外,在循环前后、分支语句、过程调用和各过程的第一句可执行语句中,调试器可以查 看到程序的状态。

-g8:类似-g3,另外,在每条源代码行中调试器都能看到程序的状态。

-g9 :类似-g8,另外,用户可以在调试器中修改用户变量来确实地改变程序行为。

使用优化调试的好处

许多 XLC/C++和 XLFortran编译器的用户为他们的产品二进制码创建单独的调试和优化版本,这是因 为他们考虑到封装调试版本给性能带来的影响和代码经优化后可调试性上的问题。我们可以将优化版本封装起 来交给客户,同时用调试版本来解决实际使用时发现的问题。虽然这是一个不错的方法,但一些开发商更希望 (甚至要求)它们的产品在生产环境中具有完全的可调试性。

我们之所以支持将可调试的二进制代码 封装进产品交给客户,是因为这确保当遇到漏洞时,客户的运行环境与技术人员在调查该漏洞时的调试环境是 一样的。优化过的代码与未优化的代码之间总是可能会存在行为上的差异,也存在优化过程自身错误地改变程 序行为的罕见情况。封装可调试的二进制代码可以解决这些问题,尤其是对于那些重视可重现性的开发商。

有了拥有新优化调试支持的Power系统下AIX和Linux平台的XLC/C++和 XLFortran编译器,你可以同时拥有 两项的优势:一个符合你实际维护需要的可调试级别,和一个缩小完全可调试性和完全优化之间差距的性能级 别。最好的一点是,相比起传统的仅支持调试而不优化的传统编译方法来说,即使现在你使用了最高级别的优 化调试,你仍可以使你的应用得到巨大的性能提升。

相关文章
  • 没有相关文章
  • 徐汉涛(www.xuhantao.com) © 2024 版权所有 All Rights Reserved.
  • 部分内容来自网络,如有侵权请联系站长尽快处理 站长QQ:965898558(广告及站内业务受理) 网站备案号:蒙ICP备15000590号-1