1. 介绍
本手册面向希望使他们的软件更快的高级程序员与软件开发者。假定读者对C++编程语言掌握良好,并对编译器如何工作有基本的了解。选择C++语言作为基础的原因在下面第8页解释。本手册主要基于我对编译器及微处理器如何工作的研究。给出的建议基于Intel,AMD与VIA的x86微处理器家族,包括64位版本。x86处理器用在大多数使用Windows、Linux、BSD与Mac OS操作系统的通用平台上,虽然这些操作系统也使用其他微处理器。许多建议可能适用其他平台,以及其他编译编程语言。
这是五本手册系列中的第一本:
- 优化C++软件:对Windows,Linux及Mac平台的优化指引。
- 优化汇编例程:对x86平台的优化指引。
- Intel, AMD及VIA CPU的微架构:对汇编程序员及编译器作者的优化指引。
- 指令表:Intel, AMD及VIA指令时延,吞吐率及微操作分解的列表。
- 不同C++编译器及操作系统间的调用惯例。
可以从www.agner.org/optimize得到这些手册的最新版。版权声明如下(原文在第168页):
本系列手册的版权为Agner Fog所有。不允许公开发行与抄袭(mirroring)。但允许对有限受众,出于教育目的非公开发行。这些手册中的样例代码可以自由使用。在我死后,GNU Free Document License将自动生效。参考www.gun.org/copyleft/fdl.html。
那些满足于以高级语言制作软件的读者,只需要读这第一本手册。后续手册面向那些希望更深入指令时序、汇编语言编程、编译器技术,以及微处理器微架构技术细节的读者。对CPU密集代码,有时更高级的优化可以通过汇编语言获得,如后续手册所述。
请注意,我的优化手册被数千人使用。我没有时间回答每个人的问题。因此,请不要把你的编程问题发给我。我不会回答。建议初学者在别处查找信息,在尝试本手册里的技术前,获得大量的编程经验。网络上有各种论坛,你可以从中得到你编程问题的答案,如果在相关书籍与手册中找不到答案。我想感谢许多告诉我勘误及建议的人。收到相关的新信息,我总是很高兴。
1.1. 优化的代价
现在大学编程的课程强调结构化与面向对象编程、模块化、可重用性以及软件开发过程的系统化的重要性。通常,这些要求与优化软件速度或大小的要求相冲突。
今天,软件教师建议函数或方法不要超过几行并不罕见。几十年前,建议是相反的:不要分出一个子例程,如果仅调用一次。这个软件编写风格转移的原因是,软件项目变得越来越大越复杂,软件开发的成本得到更多关注,以及计算机变得更加强大。
结构化软件开发的高优先性与程序效率的低优先性,首先且主要,反映在编程语言及接口框架的选择上。对不得不发明更强大的计算机以跟上持续变大软件包的终端用户,对即使简单任务,仍然被不可接受的长响应时间挫败的终端用户,这通常是不好之处。
有时,让步于先进的软件开发原则,使软件包更快、更小,是必要的。本手册讨论如何在这些考量间取得一个合理的平衡。讨论如何识别与分隔一个程序最关键部分,将优化工作集中在这个特定部分。讨论如何克服不自动检查数组越界、无效指针等相对原始编程风格的危险。还讨论相对于执行时间,哪些先进的编程构造代价高,哪些代价低。
2. 选择最优的平台
2.1. 硬件平台的选择
硬件平台的选择变得越来越不重要。RISC与CISC处理器间、PC与大型机间、简单处理器与向量处理器间的差异变得更加模糊,因为具有CISC指令集的标准PC处理器有RISC核、向量处理指令、多核,且处理速度超过了昨日的大型机。
今天,对给定任务的硬件平台选择通常由诸如价格、兼容性、第二来源及好的开发工具容易获取程度等因素决定,而不是处理能力。在一个网络里连接几台标准PC,比投资一台大型机,既便宜又更高效。具有巨大并行向量处理能力的超级计算机,在科学计算中仍然有一席之地,但对大多数任务,由于超高的性价比,标准PC处理器是优先选择。
从技术角度,标准PC处理器的CISC指令集(称为x86)不是最优的。出于与回溯到RAM内存与硬盘空间还是稀缺资源的1980前后的软件世系向后兼容的目的,这个指令集得到维护。不过,CISC指令集好于它的名声。编码的紧凑使缓存更高效,时至今日缓存大小是受限资源。在代码缓存是关键的情形下,CISC指令集实际上好于RISC。X86指令集最糟糕的问题是寄存器太少。这个问题在寄存器数加倍的x86指令集的64位扩展中得到缓解。
对关键的应用,不建议依赖于网络资源的瘦客户端,因为网络资源的响应时间不能控制。
小型手持设备变得越来越流行,越来越多用于以前要求PC的用途,比如email、网络浏览。类似的,我们看到越来越多带有嵌入式微控制器的的设备与机器。我不会给出任何关于哪些平台与操作系统对这样的应用是最高效的特定建议,不过需要知道这样的设备通常比PC的内存与计算能力要小得多。因此,在这样的系统上,经济地使用资源,比PC平台上更为重要。不过,即使在这样小的设备上,通过良好优化的软件设计,许多应用还是可能有良好性能,如第162页所述。
本手册基于使用一个Intel、AMD或VIA处理器及一个以32位或64位模式运行的Windows、Linux、BSD或Mac操作系统的标准PC平台。这里给出的许多建议也可能适用于其他平台,但例子都仅在PC平台上测试。
图形加速器
平台的选择显然受任务要求所影响。例如,一个重量级图形化应用最好在带有图形协处理器或图加速器卡的平台上实现。某些系统还有一个专用的物理处理器,用于计算计算机游戏或模拟中对象的物理位移。
在某些情形里,将图形加速器卡的高处理能力,用于屏幕渲染图形以外的目的是可能的。不过,这样的应用是高度依赖于系统,因此如果可移植性是重要的,不建议。本手册不覆盖图形处理器。
可编程逻辑设备
可编程逻辑设备是可以一个硬件定义语言,如VHDL或Verilog,编程的芯片。常见设备有CPLD与FPGA。软件编程语言,如C++,与硬件定义语言间的差别是,软件编程语言定义了顺序指令的一个算法,而硬件定义语言定义了包含数字构建块,诸如门、触发器、乘法器、算术单元等,以及连接它们的线路的硬件电路。硬件定义语言本质是并行的,因为它定义电气连接,而不是操作序列。
在一个可编程逻辑设备中,一个复杂的数字操作通常执行得比在一个微处理器中要快,因为硬件可对一个特定任务进行连线。
在一个FPGA中实现一个所谓的软处理器的微处理器是可能的。这样一个软处理器比专用的微处理器要慢得多,因此本身没有优势。但软处理器激活以硬件定义语言编写的应用特定关键指令,在某些情形里是非常高效的解决方案。一个更强大的解决方案是,在同一个芯片上结合专用微处理器核与FPGA。这样的混合解决方案现在用在一些嵌入式系统中。
我的水晶球揭示类似的解决方案某一天可能也实现在PC处理器里。应用程序将能够定义以一个硬件定义语言编写的应用特定指令。这样一个处理器除了代码缓存与数据缓存,将有一个额外缓存用于硬件定义代码。
2.2. 微处理器的选择
由于激烈的竞争,微处理器对手品牌的基准性能都十分相似。对可以分为多个并行运行线程的应用,多核处理器是有优势的。低功耗的轻量级小处理器实际上相当强悍,对不那么集约的应用可能足够。
某些系统带有图形处理器单元,在图形卡上或集成到CPU芯片里。这样的单元可用作协处理器来负责某些重量级的图形计算。在某些情形里,将图形处理单元的计算能力用于别的目的是可行的。某些系统有用于计算计算机游戏中对象位移的物理处理单元。这样一个协处理器也可用于其他目的。协处理器的使用超出本手册的范畴。
2.3. 操作系统的选择
X86家族里所有较新的微处理器都可以运行在32位及64位模式中。16位模式用在旧式操作系统DOS与Windows 3.x。如果程序或数据超过64k字节,这些系统使用内存段。这相当低效。现代微处理器不对16位模式优化,某些操作系统不向后兼容16位程序。不建议制作16位程序,除了对小嵌入式系统。
今天(2013)32位与64位操作系统都是常见的,系统间没有大的性能差异。64位软件没有很大的市场,但可以肯定64位系统将统治未来。
对某些有许多函数调用的CPU密集应用,64位系统可以提升性能5 ~ 10%。如果瓶颈在别处,32位与64位系统间的性能没有差异。使用大量内存的应用将从64位系统的大地址空间获益。
软件开发者可以选择制作两个版本的内存渴求软件。32位版本用于兼容现有系统,64位版本用于最好性能。
Windows与Linux操作系统对32位软件有几乎相同的性能,因为这两个操作系统都使用相同的函数调用惯例。FreeBSD与OpenBSD在软件优化的几乎所有方面都与Linux相同。这里关于Linux所说的一切也适用于BSD系统。
基于Intel的Mac OS X操作系统建立在BSD的基础上,但编译器缺省使用位置无关代码及延迟绑定(lazy binding),这使之较低效。可以通过使用静态链接及不使用位置无关代码(选项-fno-pic)来提升性能。
相比32位系统,64位系统有几个优势:
- 寄存器数目加倍。这使得在寄存器而不是内存中保存中间数据及局部变量成为可能。
- 函数参数在寄存器中而不是栈上传递。这使得函数调用更加高效。
- 整数寄存器的大小扩展到64位。在可以利用64位整数的应用中,这是优势。
- 分配、释放大内存块更高效。
- 在所有64位CPU与操作系统中支持SSE2指令集。
- 64位指令集支持数据的自相关取址。这使得位置无关代码更高效。
相比32位系统,64位系统有以下劣势:
- 指针、引用及栈项使用64位而不是32位。这使得数据缓存更低效。
- 在64位模式中访问静态或全局数组要求几条额外的指令用于地址计算,如果映像基址不保证小于231。这个额外的代码在64位Windows与Mac程序中可见,但在Linux中罕见。
- 在代码与数据合并大小会超过2G的大内存模型中,地址计算更复杂。虽然这个大内存模型几乎不使用。
- 某些指令在64位模式中比32位模式长1字节。
- 某些64位编译器不如它们32位的伙伴。
总的来说,如果有许多函数调用、许多大内存块分配,或者程序会利用64位整数计算,可以预期64位程序比32位程序稍微快一点。如果程序使用超过2G数据,使用64位系统是必要的。
在64位模式中运行时,操作系统间的相似性消失了,因为函数调用惯例不同。64位Windows仅允许在寄存器传递4个函数参数,而64位Linux、BSD与Mac运行最多14个参数在寄存器传递(6个整数与8个浮点)。还有其他细节使64位Linux与64位Windows中的函数调用更高效(参考手册5《不同C++编译器与操作系统的调用惯例》第50页)。具有函数调用的应用在64位Linux中稍快于64位Windows。通过内联关键函数或使其成为静态,或者使用可以进行全程序优化的编译器,可以弥补64位Windows的劣势。
2.4. 程序语言的选择
在开始一个新软件项目之前,确定哪个程序语言最适合手上的项目是重要的。低级语言擅长优化执行速度与程序大小,而高级语言擅长制作清晰与结构良好的代码,以及用户接口、网络、数据库等接口的快速与便利的开发。
最终应用的效率取决于编程语言实现的方式。当代码作为二进制可执行代码编译与发布时,获得最高效率。C++、Pascal与Fortran的大多数实现基于编译器。
其他几个编程语言通过解释实现。程序代码原样发布,在运行时逐行解释。例子包括JavaScript、PHP、ASP与Unix shell脚本。解释性代码非常低效,因为对一个循环的每次迭代,一再解释循环的主体。
某些实现使用即时(just-in-time)编译。程序代码原样发布与保存,在执行时编译。一个例子是Perl。
几种现代程序语言使用中间代码(字节码)。源代码被编译为一个中间码,它是发布的代码。中间码本身不能执行,必须通过第二步的解释或编译才可执行。Java的一些实现基于一个模拟所谓的Java虚拟机解释中间码的解释器。最好的Java机器使用代码最常用部分的即时编译。C#、managed C++,及其他在Microsoft.NET框架中的语言都基于中间码的即时编译。
使用中间码的原因是,它的目标是平台无关与紧凑。使用中间码的最大坏处是,用户必须安装一个大的运行时平台来解释或编译中间码。这个框架通常使用比代码本身多得多的资源。
中间码的另一个坏处是,它增加了抽象层次,使细致优化更困难。另一方面,即时编译器可以专门针对所运行的CPU优化,而在预编译代码中进行CPU特定优化更复杂。
编程语言与它们实现的历史揭示反映了效率、平台无关性及易开发性彼此矛盾的一段弯路。例如,第一代PC有Basic解释器。很快Basic编译器出现了,因为Basic解释器太慢。今天,最流行的Basic版本是Visual Basic.NET,它通过通过中间码与及时编译实现。Pascal的某些早期实现使用一个类似Java现在使用的字节码。在真正编译器出现时,这个语言受到了难以置信的欢迎。
从这个讨论应该清楚,编程语言的选择是效率、可移植性与开发时间之间的折衷。在效率重要时,解释性语言是不可能用的。在可移植性与易开发性比速度更重要时,基于中间码与即时编译的语言可能是可行的。这包括诸如C#、Visual Basic.NET等语言以及最好的Java实现。不过,这些语言有程序每次运行必须载入非常大运行时框架的缺点。载入框架以及编译程序所需的时间通常比执行程序所需时间长得多,并且运行时框架可能比程序本身使用更多的资源。使用这样一个框架的程序有时对简单的任务,如按下按钮或移动鼠标,有长得不可接受的响应时间。在速度是关键时,绝对应该避免.NET框架。
无疑通过完全编译代码得到最快执行。编译语言包括C、C++、Pascal、Fortran及其他几个不那么为人广知的语言。出于几个原因,我的最爱是C++。C++被几个非常好的编译器及优化的函数库支持。C++是一个先进的高级语言,带有在其他语言少见的大量先进特性。但C++语言还包括了低级C语言作为子集,给出低级优化的选择。大多数C++编译器能够产生汇编语言输出,这对检查编译器多好地优化一段代码是有用的。另外,大多数C++编译器允许类似汇编的固有函数,内联汇编,或者在需要最大程度优化时,容易与汇编语言模块链接。就C++编译器存在于所有重要平台这个事实而言,C++语言是可移植的。Pascal有C++的许多优点,但不那么全能。Fortran也相当高效,但语法太落伍了。
由于强大开发的工具,C++开发相当高效。一个流行的开发工具是Microsoft Visual Studio。这个工具可以制作C++的两个不同的实现,直接编译代码以及用于.NET框架通用语言运行时的中间码。显然,在速度重要时,首选直接编译版本。
C++一个重要的缺点与安全有关。没有数组越界、整数溢出与无效指针检查。这样检查的缺乏使代码执行得比其他进行这样检查的语言快。但在不能被程序逻辑排除的地方,显式进行这样的错误检查是程序员的责任。一些指引在下面提供,第15页。
在性能优化有高优先级时,C++绝对是首选编程语言。比其他编程语言,性能上的增益会相当显著。在性能对终端用户是重要时,性能上的增益足以弥补开发时间的小小增加。
有出于其他原因,需要基于中间码的高级框架的情形,但部分代码仍需要仔细优化。在这样的情形里,一个混合实现会是一个可行的解决方案。代码最关键部分可以编译的C++或汇编语言实现,其余代码,包括用户接口等,可以高级框架实现。代码的优化部分可以编译为由余下代码调用的动态链接库(DLL)。这不是最优的解决方案,因为高级框架仍然消耗大量的资源,两种代码类型间的转换需要消耗CPU时间的额外开销。但这个解决方案仍然可以给出相当可观的性能提升,如果时间关键的代码部分可以完全包含在一个DLL里。
另一个值得考虑的替代方案是D语言。D有许多Java与C#的特性,避免了C++的许多缺点。不过,D编译为二进制代码且可以与C或C++代码链接。用于D的编译器与IDE尚未像C++编译器那样得到良好开发。
2.5. 编译器的选择
有几个不同的C++编译器可以选择。预测一段特定的代码,哪个编译器将给出最好的优化是困难的。每个编译器做某些事情非常聪明,做其他事情非常愚蠢。一些常用编译器在下面提及。
Microsoft Visual Studio
这是一个具有许多特性、非常用户友好的编译器,但也非常贵。一个有限“特殊的(express)”版本可以免费获得。除了直接编译代码,Visual Studio还可以为.NET框架编译代码。(不使用通用语言运行时,CLR,编译产生二进制代码。)支持32位与64位Windows。集成开发环境(IDE)支持多种编程语言,性能分析与调试。C++编译器的一个命令行版本可以在Microsoft平台软件开发套件(SDK或PSDK)里免费得到。支持多核处理的OpenMP指示。Visual Studio提供相当好的优化,但它不是最好的优化器。
Borland/CodeGear/Embarcadero C++ builder
有一个具有许多与Microsoft编译器相同特性的IDE。仅支持32位Windows。不支持SSE及更新的指令集。优化不如Mcirosoft、Intel、Gnu及PathScale编译器。
Intel C++编译器(parallel composer)
这个编译器没有自己的IDE。它本意作为Microsoft Visual Studio插件用于Windows,以及Eclipse插件用于Linux。在从命令行或make应用程序调用时,它也可用作独立编译器。除了基于Intel的Mac OS与Itanium系统,它还支持32位与64位Windows以及32位与64位Linux。
Intel编译器支持向量固有函数、自动向量化(参考第110页)、OpenMP以及代码自动并行化为多个线程。编译器支持CPU分派,制作用于不同CPU的多个代码版本。(如何在非Intel处理器上进行这个工作,参考第133页)。在所有平台上,它对内联汇编都有极好的支持,并且可以在Windows与Linux里使用相同的内联汇编语法。编译器有某些优化最好的数学函数库。
Intel编译器最重要的缺点是,在AMD及VIA处理器上,编译后的代码减速执行甚至完全不能执行。通过绕过检查代码是否运行在Intel CPU上的所谓CPU分配器。有可能避免这个问题(细节参考第133页)。
对可以从其许多优化特性获益的代码,以及移植到多个操作系统的代码,Intel编译器是不错的选择。
Gnu
这是可以得到的最好的优化编译器,虽然不那么用户友好。它是免费且开源的。它伴随Linux、BSD及Mac OS X,32位与64位的大多数发布。支持OpenMP与自动并行化。支持向量固有函数与自动向量化(参考第110页)。Gnu函数库还没有完全优化。支持AMD与Intel向量数学库。Gnu C++编译器可用于许多平台,包括32位与64位Linux、BSD、Windows与Mac。对所有类Unix平台,Gnu编译器都是好的选择。
Clang
与LLVM结合在一起的Clang编译器是新的编译器,在许多方面,它类似于Gnu编译器,并与Gnu高度兼容。它被期望在Mac平台上替代Gnu编译器,不过也支持Linux与Windows平台。对所有的平台,Clang都是好的选择。
PathScale
用于32位与64位Linux的C++编译器。有许多好的优化选项。支持并行处理、OpenMP及自动向量化。在代码中插入作为编译指示(pragmas)的优化提示,告诉编译器,比如代码的一部分的执行频率。优化很好。如果不能忍受Intel编译器对Intel CPU的偏向,对Linux平台,这个编译器是一个好的选择。
PGI
用于32位与64位Windows、Linux及Mac的C++编译器。支持并行处理、OpenMP与自动向量化。优化相当好。对向量固有函数的性能极差。
Digital Mars
用于32位Windows的廉价编译器,包括一个IDE。优化不好。
Open Watcom
32位Windows的另一个开源编译器。缺省不符合标准调用惯例。优化尚可。
Codeplay VectorC
用于32位Windows的商业编译器。整合在Microsoft Visual Studio IDE里。自2004年以来,没有再更新。可以执行自动向量化。优化一般。支持3个不同的文件格式。
备注
所有这些编译器可用作没有IDE 的命令行版本。商业编译器有免费试用版本可用。
在Linux平台上,混用来自不同编译器的目标文件通常是可以的,在Windows平台上某些情形下也可以。用于Windows的Microsoft与Intel编译器,在目标文件层面,完全兼容,Digital Mars编译器与两者基本兼容。CodeGear、Codeplay与Watcom编译器,在目标文件层面,与其他编译器不兼容。
为了好的代码性能,我的建议是,对Unix应用使用Gnu、Clang、Intel或PathScale编译器,对Windows应用使用Gnu、Clang、Intel或Microsoft编译器。
编译器的选择有时由遗留代码兼容性,IDE、调试设施的特殊偏好,GUI的易开发性,数据块整合,网络应用整合,混合语言编程等决定。在选择的编译器不能提供最好的优化时,使用另一个编译器制作最关键模块可能是有帮助的。由Intel及PathScale编译器产生的目标文件,在大多数情形里,可以链接进由Microsoft或Gnu编译器创建的项目,如果包含了必要的库文件。将Borland编译器与其他编译器或函数库结合更加困难。函数必须有extern “C”声明,目标文件需要被转换为OMF格式。或者,使用最好的编译器制作一个DLL,从使用另一个编译器创建的项目中调用它。
2.6. 函数库的选择
某些应用在执行库函数上花费了大部分执行时间。耗时的库函数通常属于这些类别之一:
- 文件输入/输出
- 图形与音频处理
- 内存与字符串操作
- 数学函数
- 加解密、数据压缩
大多数编译器包含了用于许多这些目的标准库。不过,标准库不总是完全优化的。
库函数通常是由在许多不同应用中的许多用户使用的代码小片段。因此,在优化库函数上,花费比优化特定于应用的代码更多的努力是值得的。最好的函数库是高度优化的,使用适合最新指令集扩展的汇编语言与自动CPU分派(参考第125页)。
如果性能剖析(参考第16页)显示,一个特定的应用在库函数中使用了大量的CPU时间,或者这是显而易见的,那么使用不同的函数库可能会显著提高性能。如果该应用在库函数里使用了大部分时间,那么除了找出最高效的库并且充分利用该库函数调用,优化其他是不必要的。建议尝试不同的库,并检查哪个工作得最好。
某些常用函数库在下面讨论。许多用于特殊目的的库也可找到。
Microsoft
伴随Microsoft编译器。某些函数优化良好,其他没有。支持32位与64位Windows。
Borland / CodeGear / Embarcadero
伴随Borland C++ builder。没有对SSE2及更新的指令集优化。仅支持32位Windows。
Gnu
伴随Gnu编译器。优化得没有像编译器本身那么好。64位版本要好于32位版本。Gnu编译器通常插入内置(built-in)代码,而不是最常用的内存与字符串指令。内置代码不是最优的。使用选项-fno-builtin来获得库版本。Gnu库支持32位与64位Linux与BSD。Windows版本版本目前还没有更新。
Mac
包含在用于Mac OS X(Darwin)Gnu编译器的库是Xnu项目的部分。某些最重要的函数包括在称为commpage的操作系统内核中。这些函数对Intel Core及更新的Intel处理器高度优化。AMD处理器与更早的Intel处理器完全不支持。仅能运行在Mac平台上。
Intel
Intel编译器包括标准函数库。几个特殊用途库也可得到,比如“Intel Math Kernel Library”与“Integrated Performance Primitives”。这些函数库对大数据集高度优化。不过,Intel库在AMD与VIA处理器上不总是工作良好。参考133页的解释与可能的变通方案。支持所有的x86以及x86-64平台。
AMD
AMD Math核心库包含优化的数学函数。它也工作在Intel处理器上。性能不如Intel库。支持32位与64位Windows与Linux。
Asmlib
我自己为展示目的制作的函数库。可从www.agner.org/optimize/asmlib.zip获得。目前,包括内存与字符串函数,以及在别处很难找到的其他一些函数的优化版本。运行在最新的处理器上时,比大多数其他库快。支持所有的x86与x86-64平台。
函数库的比较
测试 | 处理器 | Microsoft | CodeGear | Intel | Mac | Gnu 32-bit | Gnu 32-bit -fno-builtin | Gnu 64 bit -fno-builtin | Asmlib |
memcpy 16kB对齐操作数 | Intel Core 2 | 0.12 | 0.18 | 0.12 | 0.11 | 0.18 | 0.18 | 0.18 | 0.11 |
memcpy 16kB非对齐操作数 | Intel Core 2 | 0.63 | 0.75 | 0.18 | 0.11 | 1.21 | 0.57 | 0.44 | 0.12 |
memcpy 16kB 对齐操作数 | AMD Opteron K8 | 0.24 | 0.25 | 0.24 | n.a. | 1.00 | 0.25 | 0.28 | 0.22 |
memcpy 16kB非对齐操作数 | AMD Opteron K8 | 0.38 | 0.44 | 0.40 | n.a. | 1.00 | 0.35 | 0.29 | 0.28 |
strlen 128字节 | Intel Core 2 | 0.77 | 0.89 | 0.40 | 0.30 | 4.5 | 0.82 | 0.59 | 0.27 |
strlen 128字节 | AMD Opteron K8 | 1.09 | 1.25 | 1.61 | n.a. | 2.23 | 0.95 | 0.6 | 1.19 |
表2.1. 不同函数库的性能比较 表中的数字是每字节数据核心时钟周期(数字小意味着好的性能)。对齐操作数意味着源与目标都有被16整除的地址。测试的库版本(不是最新的): Microsoft Visual studio 2008, v. 9.0 CodeGear Borland bcc, v. 5.5 Mac: Darwin8 g++ v 4.0.1 Gnu: Glibc v. 2.7, 2.8 Asmlib: v. 2.0 Intel C++ compiler, v. 10.1.020。函数_intel_fast_memcpy与__intel_new_strle在库libircmt.lib里。函数名没有文档记录。 |
2.7. 用户接口框架的选择
在一个典型的软件项目中,大部分代码定位为用户接口。不是计算密集的应用可能在用户接口上,比程序的基本任务,花费更多的CPU时间。
应用程序员很少从头编写自己的图形化用户接口。这不仅浪费程序员的时间,对终端用户也不方便。菜单、按钮、对话框等,出于可用性的原因,应该尽量标准化。程序员可以使用伴随操作系统的标准用户接口组件,或者伴随编译器及开发工具的库。
用于Windows的一个流行的用户接口是Microsoft Foundation Classes(MFC)。一个竞争产品是Borland现在停止的Object Windows Library(OWL)。Linux系统有几个图形化接口框架可用。用户接口库可以作为一个运行时DLL或静态库链接。一个运行时DLL比静态库需要更多的内存资源,除了在几个应用同时使用同一个DLL时。
用户接口库可能比应用本身更大,需要更多时间载入。一个轻量级替代是Windows Template Library(WTL)。WTL应用通常比MFC应用更快、更复杂。由于文档的缺乏以及缺少先进的开发工具,可以预期WTL应用的开发时间更长。
通过丢弃图形化用户接口,使用控制台模式程序,可以得到最简单的用户接口。控制台模式程序的输入通常在命令行或输入文件中说明。输出去往控制台或输出文件。控制台模式程序是快速、紧凑且容易开发。它很容易移植到不同的平台,因为它不依赖系统特定的图形化接口调用。可用性可能很差,因为缺乏图形化用户接口的自解释菜单。对从其他应用调用,比如make应用程序,控制台模式程序是有用的。
结论是,用户接口框架的选择一定是开发时间、可用性、程序紧凑性以及执行时间之间的权衡。没有对所有应用都最好的通用解决方案。
2.8. 克服C++语言的缺点
尽管在谈及优化时,C++有许多优势,它有某些缺点使开发者选择其他编程语言。本节讨论在为了优化选择C++时,如何克服这些缺点。
可移植性
就语法完全标准化且在所有主要平台上支持而言,C++是完全可移植的。不过,C++还是一个允许直接访问硬件接口与系统调用的语言。这些当然是特定于系统的。为了辅助平台间移植,建议在一个独立模块里放置用户接口与代码的其他系统特定部分,在另一个模块中放置代码据推测系统无关的任务特定部分。
整数的大小与其他硬件相关细节依赖于硬件平台与操作系统。细节参考第29页。
开发时间
一些开发者觉得特定的编程语言与开发工具比其他要快。尽管某些差异只是习惯问题,某些开发工具有自动进行大量琐碎编程工作的强大功能是真的。坚持模块化与可重用类,可以改善C++项目的开发时间与可维护性。
安全性
C++语言最严重的问题是关于安全性。标准C++实现不检查数组越界与无效指针。在C++程序中,这是错误的一个频繁来源,也是黑客的一个可能攻击点。有必要遵守特定的编程原则,在安全事关重要的地方,防止程序里出现这样的错误。
通过使用引用而不是指针、将指针初始化为零、一旦指向从对象无效将指针置零,以及避免指针算术与指针类型转换,可以避免无效指针的问题。可以通过更高效的容器类模板替换通常使用指针的链表与其他数据结构,如第94页所述。避免函数scanf。
数组越界可能是C++程序里最常见的错误原因。数组写越界会导致其他变量被改写,甚至更糟,它会改写数组所在函数的返回地址。这会导致各种奇怪与非预期的行为。数组通常用作保存文本或输入数据的缓冲。缺少输入数据缓冲溢出的检查是黑客经常利用的常见错误。
一个防止这样错误好的方式是,以测试良好的容器类替换数组。标准模板库(STL)是这样容器类的有用来源。不幸的是,许多标准容器类以低效的方式使用动态内存分配。如何避免动态内存分配的例子参考第91页。参考第94页关于容器类效率的讨论。本手册在www.agner.org/optimize/cppexamples.zip的附录包含了带有边界检查即各种高效容器类的例子。
文本字符串特别成问题,因为字符串长度没有确定的限度。旧的在字符数组中保存字符串的C形式方法是快且高效的,但不安全,除非在保存之前检查每个字符串的长度。这个问题的标准解决方案是使用字符串类,比如string或Cstring。这是安全且灵活的,但在大的应用中相当低效。每次创建或修改一个字符串时,字符串类分配一个新内存块。这会导致内存碎片化且需要堆管理与垃圾收集的高开销。一个不危害安全、更高效的解决方案是将所有字符串保存在一个内存池中。如何在内存池里保存字符串,参考www.agner.org/optimize/cppexamples.zip处的附录。
整数溢出是另一个安全问题。官方C标准称,在溢出的情形下,有符号整数的行为是“未定义的”、这允许编译器忽略溢出或假定它不会发生。至于Gnu编译器,假定有符号整数溢出不会发生,有允许编译器优化掉溢出检查的不幸后果。针对这个问题,有若干可能的补救方法:(1)在发生前检查溢出(2)使用无符号整数——它们保证回绕(3)使用选项-ftrapv陷入整数溢出,但这极端低效(4)使用选项-Wstrict-overflow=2,编译器对这样的优化给出警告,或者(5)使用选项-fwrapv或-fno-strict-verflow,使溢出的行为有良好的定义。
在代码速度重要的关键部分,你可能偏离上述的安全建议。如果不安全代码局限在良好测试的函数、类、模板或对程序余下部分有一个良好定义接口的模块里,这是允许的。