shangs00 2020-01-08
许多优化机会的出现源于某些C++特性被无用而导致程序运行缓慢、消耗许多资源,这些代码虽然是正确的,却不完善。这些代码往往是因为开发人员缺乏现代微处理器设备的基本常识,或是没有仔细考虑各种C++对象构建方式的性能开销而编写出的。可进行优化的另外一个原因是,C++提供了对内存管理和复制的精准控制功能。
优化是一项编码活动。在传统的软件开发过程中,直到编码完成,项目进入了集成与测试阶段,能够观察到程序整体的性能时,才会进行优化。而在敏捷开发方式中,当一个带有性能指标的特性编码完成后或是需要实现特定的性能目标时,就会分配一个或多个迭代进行优化。
Bug修复与性能优化之间的一个重要区别是,性能是一个连续变量。特性要么是实现了,要么是没有实现;Bug要么存在,要么不存在。但是可以是非常糟糕或者非常优秀,还可能是介于这两者之间的某种程度。优化还是一个迭代的过程,每当程序中最慢的部分被改善后,一个新的最慢的部分就会出现。
与其他编码任务相比,优化更像是一门实验科学,需要有更深入的科学思维方式。要想优化成功,需要先观察程序行为,然后基于这些程序行为作出可测试的推测,再进行实验得出测试结果。这些测试结果要么验证推测,要么推翻推测。经验丰富的开发人员常常相信他们对于最优代码具有足够的经验和直觉。但是除非他们频繁地测试自己的直觉,否则通常他们都是错误的。实验,而非直觉,才是贯穿本书的主题。
开发人员很难理解单个编码决策对大型程序的整体性能的影响。因此,实际上所有完整的程序都包含大量的优化机会。即使是经验丰富的团队在时间充裕的情况下编写出的代码,运行速度通常也可以提高 30%-100%。不过,通过微调代码让程序的运行速度提升10多倍几乎是不可能的,选择一种更好的算法或是数据结构,则可以将程序的某个特性的性能从慢到无法忍受提升至可发布的状态。
“不要进行优化”这条建议被过度推崇,我觉得不要过于教条,优化是没有问题的。学习高效的编程惯用法是没有问题的,即使你不知道哪部分代码的性能很重要。当你拥有良好的编程习惯,编写高效代码与编写低效无用的代码所花的时间是一样的,那为什么不优化呢?
但是,如果你不清楚重要性,因为不确定哪个算法更好而导致许多天过去了项目仍毫无进展,这是不行的。
不知道性能问题出在哪里就花费很多时间进行优化,这是不行的。试图修改程序中的每句语句去改善程序性能没有必要,也不会有作用。只有10%的代码会对程序的性能产生显著的影响,那么试图随机找出一个性能改善切入点的概率就会很低。如何使用工具定位代码中的“热点“就很重要。
实际上,常识可能是性能改善最大的敌人。常识的“解毒剂”是实验形式的科学方法。要学会利用工具测量软件性能,并通过实验验证优化效果。
C++的混合特性为我们提供了多种实现方式,一方面可以实现性能管理的全自动化,另一方面也可以对性能进行更加精准的控制。
C++有一些热点代码是性能惯犯,其中包括函数调用、内存分配和循环。以下是一份改善C++程序性能的方法总结。
使用支持C++11的编译器。C++11实现了右值引用(rvalue reference)和移动语义(move semantics),可以省去许多在以前的C++版本中无法避免的复制操作。
打开编译器的优化选项。
选择一个最优算法对性能优化的效果最大。各种优化手段都能改善程序的性能,但大部分只能使程序性能呈线性提升。除非你能找到一个更加高效的算法,否则要想实现性能的指数级增长通常是不太可能的。
优化一个糟糕的算法很愚蠢。对代码优化而言,学习和使用查找和排序的最优算法才是康庄大道。替换一个更优的算法后,数据集越大,可以缩短的运行时间就越多。
C++编译器提供的标准C++模板库和运行时库必须是可维护的、全面的和非常健壮的。我们无需对这些库进行调优。虽然C++已经发明出来30年多了,商业C++编译器的库仍然有Bug,并且可能不遵循现在的C++标准,甚至不遵循编译器发布时的标准。这使得测量和推荐优化方法的任务变得非常复杂,也使得开发人员认为没有任何优化经验是可以移植的。
有一些开源库实现了非常重要的功能,如内存管理。它们提供的复杂的实现可能比供应商提供的C++运行时库更快、更强。这些可供选择的开源库的一个优势是,它们很容易地整合至现有的工程中,并能够立即改善程序性能。
最后,开发人员还可以开发适合自己项目的库,通过放松标准库中的某些安全性和健壮性约束来换取更快的运行速度。
某些方式的函数调用的开销非常大。优秀的函数库的API所提供的函数反映了这些API的惯用法,使得用户可以无需频繁地调用其中的基础函数。
要想隐藏高度优化后的程序的复杂性,函数和类库是非常适合的地方。作为调用库的回报,它们会以最高的效率完成工作。库函数通常位于深层嵌套 调用链的底端,在那里,性能改善的效果会更加明显。
减少对内存管理器的调用是一种非常有效的优化手段,绝大多数C++语言特性的性能开销最多只是几个指令,但是每次调用内存管理器的开销却是数千个指令。
字符串是许多C++程序中非常重要和性能开销大的部分。
对缓存复制函数的一次调用也可能消耗数千个CPU周期。因此,减少复制是一种提高代码运行速度的优化方式。大量复制的发送都与内存分配有关,所以修改一处往往也会消灭另一处。其他可能会发生复制的热点代码是构造函数和赋值运算符以及输入输出。
除了内存分配和函数调用外,单条C++语句的性能开销通常都很小。但如果在循环中执行了100万次,或者每次处理时都执行这条语句,那么这就是个大问题了。绝大多数程序都会有一个或多个主要的时间处理循环和一个或多个处理字符的函数。找出并优化这些循环。
在尝试减少计算数量之前,确定程序中哪部分会被频繁地执行。
同时,现代C++编译器在进行这些局部改善方面也做得非常优秀了。因此,不应当有强迫症,将i++都替换成++i,或是展开所有的循环,不遗余力地向每位同时讲解什么是达夫设备以及它的优点。
选择最合适的数据结构对性能有着深刻的影响,因为插入、迭代、排序和检索元素的算法的运行时开销取决于数据结构。除此之外,不同的数据结构在使用内存管理器的方式上也有所不同。另一个原因是数据结构可能有也可能没有优秀的缓存本地化。
大多数程序都需要等待。任何时候,如果一个程序的处理进度因需要等待这些时间被暂停,而没有利用这些时间进行其他处理,都是一种浪费。
现代计算机都可以使用多个处理核心来执行指令。如果一项工作被分给几个处理器执行,那么它可以更快地执行完毕。
伴随并发执行而来的是用于同步并发线程让它们可以共享数据的工具。
内存管理器作为C++运行时库中的一部分,管理着动态内存分配。它在许多C++程序中都会被频繁地执行。C++确实为内存管理提供了丰富的API,虽然多数开发人员都从来没有使用过。