购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

第1章
导读

人们常说:“性能为王。”十年前如此,当然现在也未改变。根据文献(Dom,2017),2017年全球每天都在制造250亿亿 字节的数据,而根据文献(Sta,2018)的预测,这个数字每年还要增大25%。在日益以数据为中心的世界中,信息交换需求的增长促进了对更快的硬件和软件的需求。公平地讲,数据的增长不但对算力也对网络和存储系统提出了要求。

在PC时代 ,开发者通常在操作系统上直接编程,可能用到介于应用程序和操作系统之间的一些库函数。而在云计算时代,软件栈层次变得更深、更复杂,大部分开发者接触的软件栈顶层离硬件层更为遥远。栈的中间层把底层硬件做了抽象,这样当新的计算负荷出现时可以采用新型的计算加速单元。然而,这种演进的负面影响是现代应用程序开发者对运行他们软件的实际硬件不甚了解。

得益于摩尔定律,过去几十年软件开发者一直在“搭便车”。一些软件供应商更愿意等待新一代的硬件平台来提升应用程序的执行速度,而不是花费人力来优化代码。在图1中,我们可以看到单线程性能 的增长速度正在放缓。

图1 微处理器40年趋势数据(©图片来自K.Rupp的karlrupp.net网站)

图中到2010年的原始数据由M.Horowitz、F.Labonte、O.Shacham、K.Olukotun、L.Hammond和C.Batten收集和绘制,图中2010年到2015年的数据由K.Rupp收集

当每代的新硬件不再显著提升性能(Leiserson et al.,2020)时,我们就必须更加关注代码的运行速度。在寻找性能提升的方法时,开发者不应该依赖硬件,而是应该优化应用程序的代码。

“当今的软件效率非常低,因此再次成为软件程序员构建真正优化技能的黄金时代。”

—Marc Andreessen,美国企业家和投资人(a16z Podcast,2020)

个人经验 在Intel工作时,我经常听到这样的故事:当客户遇到应用程序执行慢的问题时,他们会立刻下意识地抱怨Intel的CPU太慢了。但Intel派出性能专家并跟客户一起调优应用程序后,应用程序的执行速度提升5倍甚至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. 编译器的限制 “这不是编译器该干的事情吗?为什么编译器没有解决所有的问题?”不错,当今的编译器非常智能,但是仍然会生成次优的代码。编译器很擅长消除冗余,但是当需要对诸如函数内联、循环展开等做出更复杂的决定时,编译器也许不能生成最佳的代码。例如,对于编译器是否应当将函数内嵌到调用它的代码中,并没有二选一的“是”或“否”的答案,而是依赖编译器对多种因素的综合考量。通常,编译器会根据复杂的成本模型和启发式方法进行判断,但这不能保证在所有可能场景下都正确。此外,编译器只会在确保安全且不影响生成的机器码正确性的情况下做代码优化。对编译器开发者来说,要在所有可能情况下让某些优化操作生成正确的机器码是非常困难的,所以他们通常采取保守策略以避免进行某些优化 。最后,编译器通常不会改变程序使用的数据结构,因为数据结构对性能至关重要。

3. 算法复杂度分析的限制 开发者经常过度关注算法复杂度分析,进而导致他们倾向于采用复杂度最优的流行算法,即使对给定问题而言它可能并不是性能最优的。例如,对于两个排序算法ⅠnsertionSort和QuickSort,如果采用大 O 来度量,一般而言后者会胜出:ⅠnsertionSort的时间复杂度是 O N 2 ),而QuickSort的复杂度是 O N log N )。当 N 相对较小 时,Ⅰ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)

1.2 谁需要做性能调优

在类似高性能计算(High-Performance Computing,HPC)、云计算服务、高频交易(High-Frequency Trading,HFT)、游戏开发和其他性能关键型领域中,性能工程不需要太多理由来证明其必要性。例如,谷歌报告称,搜索速度降低2%会引起每个用户的搜索量下降2%。对雅虎网站来说,页面加载时间减少400ms会带来5%~9%的新增流量。在用户数量很大的游戏场景,即使是小的性能提升也会产生显著的影响。这些例子表明,服务运行得越慢,使用它的用户越少。

有趣的是,对性能工程有需求的并不局限于上述领域。如今,通用应用程序和服务也需要性能工程,我们日常使用的工具如果不能满足性能方面的需求就无法生存下来。例如,已经集成在微软Visual Studio IDE中的Visual C++IntelliSense 功能就有非常苛刻的性能约束要求。如果IntelliSense的自动补全功能可以正常工作,它必须在几毫秒之内完成对整个源代码库的解析 。如果需要等待几秒钟才能弹出自动补全建议,那么不会有人愿意使用该代码编辑器。这样的功能必须有非常快的响应速度,并在开发者敲入新代码时能提供有效的连续性体验。类似的应用要想取得成功,只能在设计软件时就考虑到要实现这种效果的性能要求和合理的性能工程。

有时候,高性能工具会在其最初设计的目标领域之外发挥作用。例如,目前类似Unreal 和Unity 的游戏引擎在建筑、3D展示、电影制作等领域广泛使用。因为它们的性能非常出色,所以它们成了需要2D和3D渲染、物理引擎、碰撞检测、声音、动画等特性的应用程序的自然选择。

“高性能工具并不只是能让用户更快地完成任务,它还能够让工具的使用者用全新的方式完成全新类型的任务”

——Nelson Elhage博客

我不想直白地说人们痛恨运行速度慢的软件,但应用程序的性能是用户换用竞争对手产品的一个因素,重视产品的性能提升可以提高产品的竞争力。

性能工程是重要且有回报的工作,但它可能非常耗时。事实上,性能优化是一场无尽的游戏,因为总是存在值得优化的地方。不可避免地,开发者将会到达收益递减、事倍功半的时候,在这种情况下,进一步的改进需要非常高的工程成本,但是优化效果可能不值得花费如此的高成本。从这一点看,何时停下优化步伐也是性能调优工作的一个关键要点 。有些机构通过在代码审核流程中集成信息的方式来达到这一目标:源代码行用相应的“成本”指标进行注释。使用该指标来判断是否值得继续提升性能。

在开始性能调优工作之前,确保有充分的理由这样做。如果不能增加产品价值,为了优化而优化的工作纯属无用工作。合理的性能调优工作总是要先定义性能目标,论述清楚希望达到的结果以及这么做的原因。此外,还要选择度量目标是否达成的指标。关于设定性能目标的更多内容,请参考文献(Gregg,2013)和(Akinshin,2019)。

不管如何,掌握性能分析和调优的技能总是有用的。如果你阅读此书的目的就是学习这项技能,那么请继续阅读下面更精彩的章节。

1.3 什么是性能分析

你有过与同事就某一段代码的性能争论的经历吗?如果有过,那你肯定了解预测哪部分代码表现最优是一件多么困难的事。现代处理器中有非常多的变化组件,即使代码层面很小的改动都可能引发显著的性能变化。这就是为何本书给大家的第一个建议是: 一定要测量。

个人经验 我看到不少人依赖直觉来优化应用程序,他们通常在这里或那里进行随机的修复,最后却对应用程序性能没有任何实质性的影响。

经验不丰富的开发者经常修改代码,希望提升性能。例如,设想i的前一个值不会被使用,从而把i++替换成++i。这种改变通常没有任何实质效果,因为每一个合格的编译器都能识别出i的前一个值没有被使用,并且无论如何都会去除冗余的数据。

许多过去广泛流传的有效优化小技巧,已经被现代编译器学会了。此外,有些人会过度使用传统的位处理技巧,其中一个例子是使用基于XOR的变量交换,但实际上,简单的std::swap就能产生更快的代码。这些随机修改可能并不会提升应用程序的性能,正确找到需要修改的位置需要进行仔细的性能分析,而不是依靠直觉和猜测。

业界有许多性能分析方法论 ,但它们并不是总能帮助你找到方向。本书中介绍的专门针对CPU的性能分析方法有一个共同点:它们都需要收集程序运行的某些信息。程序源代码中的任何修改,都是根据对收集到的数据进行分析得出的。

定位性能瓶颈只是工程师工作的一半,而另一半工作是用合理的方法解决它。有时,改变一行程序源代码就会显著地提升程序性能。性能分析和性能优化就在于找到这一行代码并进行修改!

1.4 本书的主要内容

本书主要是为了帮助开发者理解他们所开发的应用程序的性能表现,学会寻找并去除低效代码。“为何自己写的归档工具比传统方法慢很多?为何对函数的修改引起了性能劣化?客户在抱怨程序很慢,但你不知道该从哪里入手才能解决?是否已经充分优化了程序?对于缓存未命中和分支预测错误问题,应该做些什么?”希望读完本书之后,你能得到这些问题的答案。

以下是本书内容概要:

第2 章讨论如何开展性能实验及分析实验结果,介绍性能测试和对比结果的最佳实践。

第3、4章介绍CPU微架构的基本知识和性能分析相关术语,如果你已经熟悉这些知识,可以跳过。

第5 章探讨几种流行的性能分析方法,介绍性能问题剖析方法的工作原理,以及应采集哪些数据。

第6章介绍现代CPU为支持及增强性能分析所提供的特性的相关信息,涵盖它们的工作原理以及能够解决的问题。

第7~9章介绍典型性能问题的处理方法,它们以最方便的方式与自顶向下微架构分析(Top-Down Microarchitecture Analysis,TMA)(见6.1节)一起组织和搭配使用。TMA是本书的重要概念。

第10章包含前3章中未讨论过但值得在本书中专门介绍的一些优化专题。

第11章讨论多线程应用程序的性能分析技巧,概要地描述多线程应用程序性能优化所要面对的挑战及可以使用的工具。这个主题涵盖非常广,所以这一章仅聚焦于硬件相关的问题,例如,“伪共享”。

本书提供的例子主要基于开源软件:Linux操作系统、基于LLVM/Clang的C和C++编译器、perf工具。之所以选择这些软件不仅仅是因为它们非常流行,还因为它们开放的源代码可以帮助我们理解底层工作原理,而这对学习和掌握本书中讲述的概念非常有帮助。本书也会展示某些特定领域专用的闭源重磅工具,例如Intel VTune Profiler。

1.5 本书不包含什么内容

系统性能取决于构成整个系统的不同组件,如CPU、操作系统、内存、IO设备等,而应用程序能够从不同组件的性能优化获益。通常来说,工程师应当分析整个系统的性能,然而,影响整个系统性能的最大因素是其核,即CPU。这也是本书从CPU视角探讨性能分析的原因,当然,在个别情况下也会涉及操作系统、内存等子系统。

本书讨论的问题只局限于单CPU,也就是说,本书不讨论分布式、NUMA、异构计算系统的优化技术,也不讨论使用OpenCL、OpenMP等框架平台将计算卸载到加速器(GPU、FPGA等)的技术。

本书围绕Intel x86 CPU架构平台展开,不提供AMD、ARM、RISC-V等芯片架构的优化办法,但是本书运用的调优技巧同样适用于这些处理器。另外,本书中的案例以Linux操作系统为平台,但是同样的技巧实际上也可以应用于Windows、Mac等操作系统。

本书中所有的代码片段都以C、C++以及x86汇编语言编写,但是绝大部分的思想也可以用于使用类似Rust、Go甚至Fortran语言编写的代码。由于本书面向在用户态运行的程序,直接接近硬件本身,因此我们不讨论管理环境(例如Java)下的问题。

最后,本书作者假定读者能完全控制自己开发的软件,包括选择库函数、编译器等。因此,本书不包括对某些商用软件包的调优,例如优化SQL数据库查询。

1.6 本章总结

硬件在单线程性能方面没有像过去那样获得那么多的性能提升,这就是性能调优变得比过去40年时更重要的原因,如今计算行业的变化比20世纪90年代以来的任何时候都要剧烈。

根据文献(Leiserson et al.,2020),在不远的将来软件调优将成为性能提升的关键驱动力,所以不可以低估性能调优的重要性。对于大型分布式应用程序来说,每个细小的性能优化都可能节省巨大的成本。

软件并不默认具有最优的性能,某些因素会限制应用程序发挥最大潜能,而这些限制因素存在于软件、硬件环境。CPU并不会神奇地加速慢算法,编译器也不能为每个程序都产生最优的机器码。由于硬件的特殊性,针对特定问题的著名算法并不一定总是能够达到期望的效果。所有这些因素都为软件调优工作留下了发挥空间。

对某些应用程序来说,性能不只是一个简单的功能特性,它还能够让用户用全新方法解决全新的问题。

软件优化需要有强烈的业务需要支撑,开发者应该设定可量化的目标和指标来衡量进度。

现代计算平台下,各种因素掺杂在一起,预测某段代码的性能几乎不可能。开展软件调优工作时,开发者不应当依赖直觉,而应当使用细致的性能分析。 n0YcIFTBd34Aashub2xPoGwVO1qhr7Edh3GEshkbfhhZdpgVguDUUmMupkqngS6W

点击中间区域
呼出菜单
上一章
目录
下一章
×