下面的图5.10 描述了此过程,该过程包括四个步骤:
1.表征应用程序。
2.优先进行编译器优化。
3.选择基准。
4.评估编译器优化的性能。
图5.10:编译器优化过程
使用编译器的优化始于 对应用程序的表征。此步骤的目标是确定可能优先使用一种优化而不是另一种优化的代码属性,并帮助确定要尝试的优化的优先级。
如果应用程序很大,则可能会受益于高速缓存的优化。如果应用程序包含浮点计算,则自动矢量化可能会带来好处。下表5.4 总结了要考虑的问题和根据答案得出的结论示例。
表5.4:应用程序特征
第二步是 基于对哪些优化可能会带来有益的性能提升的了解来确定编译器优化设置的测试优先级。性能运行需要花费时间和精力,因此至关重要的是优先考虑可能会提高性能的优化,并预见在应用这些优化过程中可能遇到的任何挑战。
例如,某些高级优化需要更改构建环境。如果要衡量这些高级优化的性能,则必须愿意花时间进行这些更改。至少,所需的努力可能会降低优先级。另一个例子是更高的优化对调试信息的影响。
通常,更高的优化会降低调试信息的质量。因此,除了评估过程中评估性能外,还应考虑对其他软件开发要求的影响。如果调试信息降级到不可接受的水平,则可以决定不使用高级优化,也可以研究可以改善调试信息的编译器选项。
第三步,选择基准,包括为您的应用程序选择一个小的输入集,以便可以比较使用不同优化设置编译的应用程序的性能。选择基准时,应牢记以下几点:
1) 基准测试运行应具有可重复性,例如,每次运行的时间不会有显着差异。
2) 基准测试应在短时间内运行,以允许运行许多性能实验;但是,执行时间不能太短,以至于使用相同优化的运行时间差异很大。
3) 基准应该代表您的客户通常的经营状况。
最后一步是使用所需的优化来构建应用程序,运行测试并评估性能。测试应至少进行3次。我的建议是放弃最慢和最快的时间,并以中间时间为代表。
我还建议您在获得结果时检查结果,看看实际结果是否符合您的期望。如果执行性能测试的时间很长,则您可以在其他地方分析和验证收集的运行结果,并尽早发现任何错误或遗漏的假设。
最后,如果测得的性能满足您的性能目标,那么是时候将构建更改投入生产了。如果性能未达到您的目标,则应考虑使用性能分析工具(例如Intel VTune Performance Analyzer)。
通过阅读本系列的第1部分,您应该记住一个关键点:让编译器为您完成工作。
有许多书籍向您展示如何手动执行优化,例如源代码中的展开循环。现在,编译器技术已经到了可以在大多数情况下确定何时进行循环展开的时间。
在开发人员有知识的情况下,编译器无法确定特定的代码段,通常会有一些指令或编译指示可以在其中为编译器提供缺少的信息。
要查看此过程的工作情况,请下载案例研究的PDF,以了解如何将其与MySQL开放源数据库一起使用。
C / C ++编译器的可用性
在开始进行并行化之前,需要改进的另一个领域是C和C ++编译器的可用性。由于以下原因,开发环境(尤其是C和C ++编译器)的性能可提高开发人员的性能:
1)更高的标准合规性和诊断能力可以减少编程错误。
2)更快的编译时间导致每天更多的构建。
3)较小的应用程序代码大小可以使应用程序提供更多功能。
4)代码覆盖率工具可帮助构建强大的测试套件。
生成代码的性能是编译器的关键属性。但是,还有其他一些属性归为可用性属性,这些属性在确定易用性以及最终开发成功方面起着关键作用。
接下来的几节从以下方面描述了增加编译器使用的技术:1) 诊断,2) 兼容性,3) 编译时间,4) 代码大小和5) 代码覆盖率。如果正确采用这些技术,则可以在嵌入式项目的开发阶段提高开发人员的效率并节省相关的成本。
诊断。 用于嵌入式软件开发的两种广泛使用的语言是C和C ++。这些语言中的每一种都已由各种组织标准化。例如,ISO C一致性是编译器遵守ISO C标准的程度。当前,市场上常见两种ISO C标准:C89 [4] 和C99 [5] 。
C89当前是C应用程序的事实上的标准。但是,C99的使用正在增加。ISO C ++标准[6]于1998年获得批准。C++语言是从C89派生的,并且与C89在很大程度上兼容。两种语言之间的差异总结在ISO C ++标准的附录C中。
由于许多异常神秘的功能(例如异常规范和导出模板),很少有C ++编译器提供100%的C ++标准一致性。大多数编译器提供了一个选项来指定更严格的ISO一致性程度。
使用选项来强制执行ISO一致性可以帮助可能对将其应用程序移植到多个编译器环境中的开发人员感兴趣。
编译器诊断程序也有助于移植和开发。许多编译器提供了一个选项来诊断代码中的潜在问题,这些问题可能导致难以调试运行时问题。在代码开发过程中使用此选项可以减少调试时间。
另一项有用的诊断功能是任意诊断。任意诊断程序具有与之相关的编号,该编号允许在停止编译错误消息,警告消息,备注或无诊断之间升级或降级诊断。下表5.8说明 了适用于Linux的英特尔C ++编译器中提供的选项,这些选项提供了上述功能。
表5.8:诊断选项说明
某些编译器包括一个配置文件,该文件允许在编译期间放置其他选项,并且可以作为进行其他任意诊断的有用位置。
例如,为了 将所有C编译的警告#1011提升为错误, 在配置文件中放置-we1011无需手动将选项添加到每个编译命令行。有关诊断功能(包括可用性和语法)的完整信息,请参见编译器的参考手册。
兼容性。 嵌入式软件开发中的一个挑战是将针对一种架构设计和优化的软件移植到同一系列中的新升级架构上。
一个示例是将运行您的应用程序的系统从基于Intel Pentium III处理器的系统升级到基于Intel Pentium 4处理器的系统。在这种情况下,您可能需要重新编译软件并针对新架构及其性能功能进行优化。
一种备受重视的编译器功能是能够针对新架构进行优化,同时保持与旧架构的兼容性。处理器调度技术解决了升级架构的问题,同时保持了与现场部署的传统硬件的兼容性。
英特尔已经发布了许多指令集扩展,例如MMX技术(多媒体扩展)和流SIMD扩展(SSE,SSE2和SSE3)。例如,Intel Pentium 4处理器支持所有这些指令集扩展,但是较旧的处理器(例如Pentium III微处理器)仅支持这些指令的一部分。
当应用程序在奔腾4处理器上执行时,处理器分派技术允许使用这些新指令;当应用程序在不支持指令的处理器上执行时,处理器分派技术指定备用代码路径。
可以使用三种主要技术使开发人员根据执行体系结构调度代码:
1)丘比特的显式编码:程序员可以向标识执行处理器的函数添加运行时调用,并根据结果调用不同版本的代码。
2)编译器手动处理器分派: 语言支持,用于为每个感兴趣的处理器指定不同版本的代码。下面的图5.11 是一个手动处理器分发示例,该示例在Pentium III处理器上执行时指定一个代码路径,而在其他处理器上执行时指定一个代码路径。
3)自动处理器分派: 编译器确定使用新指令的收益,并为每个感兴趣的处理器自动创建多个版本的代码。
编译器插入代码,以将调用分派到该函数的版本,该版本取决于当时正在执行应用程序的处理器。这些处理器调度技术可帮助开发人员利用较新处理器的功能,同时保持与较旧处理器的向后兼容性。
使用这些技术的缺点是您的应用程序中的代码大小增加。同一例程有多个副本,只有一个副本将在特定平台上使用。提醒开发人员注意代码大小的影响,并考虑仅将技术应用于应用程序中的关键区域。
图5.11:手动分派示例
编译时间。 编译时间定义为编译器完成应用程序编译所需的时间。编译时间受许多因素影响,例如源代码的大小和复杂性,使用的优化级别以及主机速度。以下各节讨论了两种在应用程序开发过程中缩短编译时间的技术。
PCH文件。 在应用程序项目中的几个源文件包含公共头文件的子集的情况下,预编译头(PCH)文件可以缩短编译时间。在处理了一组形成预编译头文件集的头文件之后,PCH文件本质上是编译器的内存转储。
PCH文件通过消除不同源文件对相同头文件的重新编译来缩短编译时间。通过观察到许多源文件在源文件的开头都包含相同的头文件,使PCH文件成为可能。
图5.12:预编译头文件的源示例
上面的图5.12 显示了一种安排头文件以利用PCH文件的方法。首先,公共头文件集是由项目中其他文件包含的单个头文件包含的。
在下面的示例中,global.h 包含通用头文件集的include伪指令,并且包含在源文件file1.cpp和file2.cpp中。文件file2.cpp还包含另一个头文件vector。
使用编译器选项可以在第一次编译期间创建PCH文件,并在以后的编译中使用PCH文件。在使用自动PCH文件进行编译期间,编译器将解析包含文件,并尝试将头文件的顺序与已创建的PCH文件进行匹配。
如果可以使用现有的PCH文件,则将加载PCH文件头并继续编译。否则,编译器将为包含的文件列表创建一个新的PCH文件。#pragma hdrstop 用于告诉编译器尝试将pragma上方的源文件中的包含文件集与现有PCH文件进行匹配。
对于file2.cpp,使用#pragma hdrstop 允许使用file1.cpp使用的同一PCH文件。请注意,在某些情况下,创建和使用太多唯一的PCH文件实际上会减慢编译速度。
应用PCH文件时,用于图5.12中的代码的特定编译命令为:
icc -pch -c file1.cpp
icc -pch -c file2.cpp
尝试 在命令行上使用-pch选项和不使用-pch选项来计时。下表5.9 显示了图5.12和其他两个应用程序中的代码的编译时间减少。
表5.9:使用PCH减少编译时间
POV-Ray [7] 是用于创建复杂图像的图形应用程序。 图形应用程序EON [8]是SPEC CINT2000中的基准应用程序。这些测试在带有512 MB RAM的Red Hat Linux的基于2.8 GHz Pentium 4微处理器的系统上运行。
并行构建。 减少编译时间的第二种技术是使用并行构建。支持并行构建的工具的一个示例是make(带有 -j 选项)。
下表5.10 显示了使用make -j2 命令在双处理器系统上构建POV-Ray和EON的减少编译时间。这些测试在具有512 MB RAM和Red Hat Linux的基于3.2 GHz Pentium 4微处理器的双系统上运行。
表5.10:使用make -j 2减少编译时间
代码大小 。编译器在嵌入式应用程序的最终代码大小中起着关键作用。开发人员可以掌握一些会影响代码大小的编译器优化设置的知识,并且在代码大小很重要的情况下,可以通过谨慎使用这些选项来减小代码大小。
许多编译器提供了针对大小进行优化的优化开关。两个-O 选项专门针对代码大小进行优化。选项 -O1可 优化代码速度和代码大小,但可以禁用通常会导致更大代码大小的选项,例如循环展开。
选项-Os 以牺牲代码速度为代价来优化代码大小,并且可能在优化过程中选择代码序列,从而导致比其他已知序列更小的代码大小和更低的性能。
除了 上面的-O选项可以启用或禁用优化集之外,开发人员还可以选择禁用特定代码大小增加的优化。一些可用的选项包括:
1)用于分别关闭常见代码大小以增加优化的选项,例如循环展开。
2)禁用代码或库中函数内联的选项。
3)链接到较小版本的库(如精简C ++库)中的选项。
最后的代码大小建议与本章第一部分中详细介绍的高级优化有关。自动矢量化和过程间优化可能会增加代码大小。
例如,自动向量化可以将一个循环变成两个循环:一个循环可以一次迭代该循环的多次迭代,另一个循环可以处理清理循环。通过过程间优化执行的内联可能会增加代码大小。
为帮助减轻自动矢量化或过程间优化所观察到的代码大小增加,请使用配置文件引导的优化,使编译器仅在收益最大的情况下使用这些优化。下表5.11 总结了常见代码大小优化的列表。
表5.11:与代码大小相关的选项
在不需要C ++异常处理的情况下,应谨慎应用选项-fnoexception。许多嵌入式开发人员希望使用C ++语言功能(例如对象),但可能不希望异常处理的开销。
将应用程序中不需要的符号“剥离”也是一种很好的做法。在动态绑定的上下文中,需要一些符号来执行重定位;但是,通过使用(在Linux系统上)可以消除不需要的符号
不需要带状
代码覆盖率。 许多嵌入式开发人员在他们的嵌入式应用程序中使用了一组通用输入的测试套件,这使人确信新实现的代码不会破坏现有的应用程序功能。测试套件的一个共同愿望是,它要执行尽可能多的应用程序代码。
代码覆盖范围是由提供此信息的编译器启用的功能。具体来说,代码覆盖率提供了给定一组特定输入后执行的应用程序代码的百分比。概要文件引导的优化中使用的相同技术可启用某些代码覆盖率工具。
下面的图5.13 是报告示例程序的代码覆盖率工具的屏幕截图。该工具能够突出显示由于运行应用程序输入而未执行的功能。
该工具还可以突出显示已执行功能中尚未执行的区域。然后,测试套件开发人员可以使用该报告来添加提供覆盖率的测试。考虑使用代码覆盖率工具来提高测试套件的效率。
图5.13:代码覆盖工具
优化对调试的影响
调试是软件开发中一个容易理解的方面,通常涉及软件工具的使用,该调试器提供:1) 跟踪应用程序的执行;2) 能够在应用程序的指定位置停止执行;以及3) 能够在执行期间检查和修改数据值。
优化应用程序的权衡之一是调试信息的普遍丢失,这使调试更加困难。优化对调试信息的影响本身就是一个大话题。简而言之,由于以下原因,优化会影响调试信息:
1) 许多优化的x86应用程序都使用基指针(EBP)作为通用寄存器,这使得进行准确的堆栈回溯变得更加困难。
2) 代码运动允许散布来自源代码不同行的指令。在这种情况下,按顺序单步执行源代码行是不可能的。
3) 内联使代码运动问题更加复杂,因为源代码的顺序步进可能会导致显示几个不同的源位置,从而导致潜在的混乱。
表5.12:调试相关选项
上面的表5.12 描述了几个与调试相关的选项。这些选项特定于Linux上的Intel C ++编译器。但是,其他编译器中也存在类似的选项。
请查阅您的编译器文档以确定可用性。的-g 选项是标准调试选项,使在调试会话期间要被显示的源信息。
如果没有此选项,调试器将只能显示应用程序的汇编语言表示。列出的其他选项与-g 选项结合使用,并提供列出的功能。在调试优化的代码时,请考虑使用补充调试选项。