为什么我们仍然需要性能调优?
现代CPU每年都在增加越来越多的核心。截至2019年底,您可以购买到一款高端服务器处理器,其逻辑核心数量超过100个。这非常令人印象深刻,但这并不意味着我们不再需要关注性能。很多时候,随着CPU核心数量的增加,应用程序的性能可能并不会提升。典型的通用多线程应用程序的性能并不总是随着我们分配给任务的CPU核心数量线性增长的。了解为什么会发生这种情况以及可能的解决方法对于产品未来的增长至关重要。不能进行适当的性能分析和调优会导致性能和金钱的浪费,并可能毁掉产品。
根据[@Leisersoneaam9744],至少在短期内,大多数应用程序的性能收益的很大一部分将来自软件堆栈。可悲的是,应用程序默认情况下并不会获得最佳性能。该论文还提供了一个很好的例子,说明了在源代码级别上可以进行的性能改进潜力。在表@tbl:PlentyOfRoom中总结了对两个4096×4096矩阵相乘的程序进行性能工程的加速效果。通过应用多个优化,最终得到的程序运行速度提高了60000多倍。提供这个例子的原因不是挑剔Python或Java(它们是很好的语言),而是打破了软件默认具有“足够好”的性能的观念。
版本 | 实现 | 绝对加速 | 相对加速 |
---|---|---|---|
1 | Python | 1 | - |
2 | Java | 11 | 10.8 |
3 | C | 47 | 4.4 |
4 | 并行循环 | 366 | 7.8 |
5 | 并行分治 | 6,727 | 18.4 |
6 | 加向量化指令集 | 23,224 | 3.5 |
7 | 加 AVX 指令集 | 62,806 | 2.7 |
说明:
- 绝对加速:相对于 Python 版本的运行时间,该版本的运行时间减少了多少。
- 相对加速:相对于上一版本的运行时间,该版本的运行时间减少了多少。
结论:
- 使用并行分治可以获得最大的性能提升。
- 使用向量化指令集也可以获得显著的性能提升。
- 使用 AVX 指令集可以进一步提升性能,但幅度较小。
表: 在60GB内存,双插槽(socket) Intel Xeon E5-2666 v3系统上运行的一个程序,该程序执行两个4096×4096矩阵相乘的加速效果。来源:[@Leisersoneaam9744]。
以下是阻止系统默认达到最佳性能的一些最重要因素:
CPU限制:很容易问:“为什么硬件不能解决我们所有的问题?”现代CPU以令人难以置信的速度执行指令,并且每一代都在变得更好。但是,如果用于执行工作的指令不是最佳的,甚至是多余的,它们就无法做太多事情。处理器不能通过魔法将次优代码转换为性能更好的代码。例如,如果我们使用BubbleSort算法实现排序例程,CPU将不会尝试识别并使用更好的替代方案,例如QuickSort。它会盲目地执行被告知要执行的任何操作。
编译器限制:“但是编译器不是应该做这些吗?为什么编译器不能解决我们所有的问题?”的确,现在的编译器非常智能,但仍然可能生成次优代码。编译器擅长消除冗余工作,但是当涉及到更复杂的决策,如函数内联、循环展开等时,它们可能不会生成最佳的代码。例如,是否应该将一个函数始终内联到调用它的地方,这个问题没有二元的“是”或“否”答案。这通常取决于编译器应该考虑的许多因素。通常,编译器依赖于复杂的成本模型和启发式算法,这些算法可能不适用于每种可能的情况。此外,除非编译器确定这样做是安全的,并且不会影响生成的机器代码的正确性,否则它们不能执行优化。对于编译器开发人员来说,确保特定优化在所有可能的情况下生成正确的代码可能非常困难,因此他们通常必须保守行事,并避免进行一些优化。最后,编译器通常不会转换程序使用的数据结构,这在性能方面也是至关重要的。
算法复杂度分析限制:开发人员经常过度关注算法的复杂度分析,这导致他们选择具有最优算法复杂度的流行算法,即使它对于给定问题可能不是最有效的。考虑两种排序算法,插入排序和快速排序,后者在平均情况的大O符号中显然更胜一筹:插入排序是O(N^2),而快速排序仅为O(N log N)。然而,对于相对较小的
N
值(最多50个元素),插入排序的性能优于快速排序。复杂度分析无法考虑各种算法的分支预测和缓存效果,因此人们只是将它们封装在隐含的常数C
中,有时这可能会对性能产生重大影响。盲目地信任大O符号而没有在目标工作负载上进行测试可能会使开发人员走上错误的道路。因此,对于某个特定问题来说,最知名的算法并不一定是实践中最高效的。
以上所述的限制留下了调优我们的软件以发挥其全部潜力的余地。广义上来说,软件堆栈包括许多层,例如固件、BIOS、操作系统、库以及应用程序的源代码。但由于大多数较低的软件层不在我们的直接控制之下,因此重点将放在源代码上。我们将经常涉及的另一个重要的软件是编译器。通过让编译器生成所需的机器代码,可以获得令人满意的加速效果,通过各种提示方法。您将在本书中找到许多这样的例子。
[!TIP|style:flat|label:作者个人经验] 要成功地在您的应用程序中实现所需的改进,您不必成为编译器专家。根据我的经验,至少90%的所有转换都可以在源代码级别完成,而无需深入研究编译器源代码。尽管如此,了解编译器的工作原理以及如何使其按照您的意愿进行操作在与性能相关的工作中始终是有利的。
此外,如今,使应用程序能够通过将其分布在许多核心上扩展是至关重要的,因为单线程性能往往会达到一个平台。这种启用需要应用程序各个线程之间的有效通信,消除资源的不必要消耗以及其他多线程程序常见的问题。
值得一提的是,性能收益不仅仅来自调整软件。根据[@Leisersoneaam9744],未来的两个主要潜在速度提升源是算法(特别是对于机器学习等新问题领域)和简化的硬件设计。算法显然在应用程序的性能中发挥了重要作用,但我们不会在本书中讨论这个主题。我们也不会讨论新硬件设计的主题,因为大多数情况下,软件开发人员必须处理现有的硬件。然而,了解现代CPU设计对于优化应用程序是重要的。
“在摩尔定律后的时代,使代码运行快速变得越来越重要,尤其是使其适合运行的硬件。” [@Leisersoneaam9744]
本书的方法论侧重于从应用程序中挤出最后一点性能。这种转变可以归因于表@tbl:PlentyOfRoom中的第6行和第7行。将讨论的改进类型通常不大,并且通常不超过10%。然而,不要低估10%的性能提升的重要性。这对于在云配置中运行的大型分布式应用程序尤为重要。根据[@HennessyGoogleIO],在2018年,谷歌在运行云的实际计算服务器上花费的资金大致与花费在电力和冷却基础设施上的资金相同。能源效率是一个非常重要的问题,可以通过优化软件来改善。
“在这种规模下,理解性能特征变得至关重要 - 即使是性能或利用率的小幅改进也可以转化为巨大的成本节省。” [@GoogleProfiling]