现代CPU性能分析与优化
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

1.1 为什么需要性能调优

现代CPU的核数量每年都在增长,到2019年底,我们可以购买到具有100多个逻辑核的高端服务器处理器。虽然令人惊叹,但这并不意味着我们无须关心性能问题,我们经常看到的情况是应用程序性能可能不会随着CPU核数量的增加而提升。典型的通用多线程应用程序的性能并不总是随着分配到任务的CPU核数量的增长而线性增长,了解发生这种情况的原因及可能的解决方案对产品的未来发展至关重要。产品性能若不能被恰当地分析和调优,可能会导致大量的性能和资金浪费,甚至可能导致产品最终失败。

据论文(Leiserson et al.,2020)介绍,至少在近期,大部分应用程序的性能提升都源自软件栈。但是很不幸,应用程序并不会默认得到最优的性能。在论文(Leiserson et al.,2020)中,作者提供了一个很好的例子,描绘了在源代码层面进行性能提升的潜力。表1总结了两个4096×4096矩阵相乘的程序经性能工程优化后的加速效果。经过多种优化后的最终结果是程序运行速度提升了60000多倍。举这个例子并不是为了让你选择Python或者Java语言(它们都是非常优秀的编程语言),而是为了打破默认情况下软件就有“足够好”性能的印象。

表1 通过性能工程加速两个4096×4096矩阵相乘的程序

注:运行于双插槽60 GB内存Intel Xeon E5-2666 v3系统上,摘自论文(Leiserson et al.,2020)。

以下是影响系统在默认情况下获得最佳性能的一些重要因素:

1. CPU的限制 人们经常会忍不住问:“硬件为何不能解决这一切性能问题?”现代CPU以惊人的速度执行指令,并且每一代都在变得更好。但是,如果执行任务的指令不是最优的甚至是多余的,CPU也无能为力,处理器并不能神奇地把次优的代码转化为性能更好的代码。例如,如果我们用冒泡算法BubbleSort实现排序程序,CPU无法识别出它是排序算法的实现并替换为更好的算法(如快速排序算法QuickSort)。CPU盲目地执行被告知的任务。

2.编译器的限制 “这不是编译器该干的事情吗?为什么编译器没有解决所有的问题?”不错,当今的编译器非常智能,但是仍然会生成次优的代码。编译器很擅长消除冗余,但是当需要对诸如函数内联、循环展开等做出更复杂的决定时,编译器也许不能生成最佳的代码。例如,对于编译器是否应当将函数内嵌到调用它的代码中,并没有二选一的“是”或“否”的答案,而是依赖编译器对多种因素的综合考量。通常,编译器会根据复杂的成本模型和启发式方法进行判断,但这不能保证在所有可能场景下都正确。此外,编译器只会在确保安全且不影响生成的机器码正确性的情况下做代码优化。对编译器开发者来说,要在所有可能情况下让某些优化操作生成正确的机器码是非常困难的,所以他们通常采取保守策略以避免进行某些优化[4]。最后,编译器通常不会改变程序使用的数据结构,因为数据结构对性能至关重要。

3.算法复杂度分析的限制 开发者经常过度关注算法复杂度分析,进而导致他们倾向于采用复杂度最优的流行算法,即使对给定问题而言它可能并不是性能最优的。例如,对于两个排序算法ⅠnsertionSort和QuickSort,如果采用大O来度量,一般而言后者会胜出:ⅠnsertionSort的时间复杂度是ON2),而QuickSort的复杂度是ONlogN)。当N相对较小[5]时,ⅠnsertionSort比QuickSort表现更好。复杂度分析无法解释各种算法的所有分支预测和缓存的影响,所以只是将它们封装成一个隐含的常数C,有时这会对性能产生巨大的影响。不经过对目标负荷的测试而盲目地信任大O度量,会让开发者误入歧途。对某个问题,即使最知名的算法也不一定能在所有输入情况下都性能表现最佳。

上述限制为软件性能调优以充分发挥其潜力提供了空间。广义来说,软件栈包含很多层,例如,固件、BIOS、操作系统、函数库和应用程序源代码。但是,由于大多数底层软件都不在我们的直接控制范围内,因此我们主要聚焦在源代码上。另外一个经常接触的软件组件是编译器,通过各种注解,编译器可以生成让程序性能显著提高的机器码,本书将给出很多这样的例子。

个人经验 即使你并不是编译器专家,也能成功地提升应用程序的性能。根据我的经验,至少90%的转换可以在源代码层面完成,而无须深入研究编译器源代码。尽管如此,理解编译器的工作原理,以及懂得如何能够让编译器为你所用还是非常有帮助的。

此外,在当前单线程性能已经达到峰值并趋平时,把应用程序分布式运行于多个计算核是很有必要的。而这就要求程序的不同线程之间能高效通信、排除不需要的资源消耗并规避多线程程序的典型问题。

特别需要指出的是,性能的提升不止来自软件调优。据论文(Leiserson et al.,2020)介绍,未来另外两个主要潜在加速源是算法(尤其对机器学习等新问题领域)和高效的硬件设计。显然,算法对应用程序性能有显著的影响,但是在本书中我们不讨论这个主题。因为大多数时候,软件工程师都是在现有硬件平台上开发,所以我们也不讨论新的硬件设计。当然,理解现代CPU设计对应用程序调优很重要。

“在后摩尔定律时代,让代码运行得更快(尤其根据运行它的硬件进行定制)变得更加重要。”(Leiserson et al.,2020)

本书中的方法论聚焦于从应用程序中挤出最后一点性能潜力,这类转换方法可以参考表1中的第6行和第7行。将要讨论的优化方法对性能的提升通常不会很大,一般不超过10%,但是千万不要低估这10%的提升的重要性,特别是对那些运行在云环境下的大型分布式应用程序。根据文献(Hennessy,2018)的介绍,在2018年,谷歌在运行云的计算服务器上实际花费的费用几乎与其在电力和冷却基础设施上的花费相同。能源效率是一个很重要的问题,而它可以通过优化软件来改善!

“在这样的规模下,理解性能特征变得很关键——即使性能或利用率上很小的改善也可以转化为巨大的成本节约。”(Kanev et al.,2015)