有时看到修改后程序的运行时间发生变化时,却不清楚具体原因是什么。单独的时间信息有时无法给出问题发生的根本原因。
程序运行时硬件和软件都可以采集性能数据,硬件是指运行程序的CPU,软件是指操作系统和所有可用于分析的工具。通常软件栈提供上层指标,比如时间、上下文切换次数和缺页次数,而CPU可观察缓存未命中、分支预测错误等。根据要解决问题的问题,各指标的重要程度不一样。性能分析工具Linux perf可同时使用来自操作系统和CPU的数据。
本章将介绍一些流行的性能分析方法,如代码插桩、追踪、表征和采样,也会讨论静态性能分析方法、以及不需运行实际应用程序的编译器优化报告。
5.1 代码插桩
代码插桩可能是第一个被发明的性能分析方法,它通过在程序中插入额外的代码来采集运行时信息。
基于插桩的剖析方法常被用在宏观层次,而不是微观层次。在优化大段代码的场景,使用该方法通常会给出很好的洞察结果,因为你可以自上而下(现在主函数插桩,然后再往被调函数插桩)地定位性能问题,对于不熟悉代码库的人来说特别有用。
优化对象不仅是代码、也是数据。例如,渲染太慢是因为没有压缩网格,物体运动太慢是因为场景中有太多对象。
插桩的缺点:
1. 它并不能提供任何关于代码如何操作系统或CPU角度执行的信息。例如,进程调度执行的概率或者发生多少次分支预测错误的信息。
2. 每当需要插桩新内容(比如一个变量)时,都需要重新编译。这可能成为工程师的负担,增加分析时间。
3. 在热点代码插入代码可能会导致整个基准测试的速度降低为原来的1/2。
4. 可能改变程序的行为,无法看到之前相同的现象。
手动插桩会增加实验之间的时间间隔,并消耗很多的开发时间。于是自动化插桩被广泛在编译器中使用。编译器自动对整个程序进行插桩,并收集和运行相关的统计信息,经典的有代码覆盖度检查和基于剖析文件的编译优化。
值得一提的是,二进制插桩是在已经构建的可执行文件上完成的,而不是在源代码上完成。二进制插桩有2种类型:静态插桩(提前完成)和动态插桩(在程序执行时按需插桩)。动态二进制插桩的主要优点是它不需要程序重新编译和链接。此外,使用动态插桩可以将插桩限制在感兴趣的代码区域,而不是用在整个程序。
Intel Pin在发生被关注事件时,Pin会拦截程序的执行,并从该点开始生成新的插桩代码,它可以收集各种运行时信息,例如:
1. 指令计数和函数调用计数;
2. 拦截应用程序中函数调用和指令的执行;
3. 通过捕获程序区域开头的内存和硬件寄存器状态,可以实现程序区域的“记录和重放”;
5.2 跟踪
代码插桩假设开发者可以掌控程序的代码。然而,跟踪依赖于程序外部依赖项的现有插桩。Linux的strace是内核的插桩。Intel Process Traces是CPU的插桩。跟踪可以从预先插桩好并且不容易改变的组件中获得,所以跟踪通常用在黑盒场景,即用户不能修改应用程序代码,但是又想深入了解程序在幕后做了什么的场景。
跟踪的开销取决于要跟踪的目标,系统调用多,开销大,反之则小。为了弥补这个缺陷,跟踪工具提供了过滤功能,可以只采集指定时间段或指定代码段的数据。
与代码插桩类似,通常跟踪也是为了探究系统中的异常。例如你可能想知道10s的无响应的时间中应用程序发生了什么。
跟踪技术对于调试工作非常有用,它的基本特性支持基于已记录的踪迹来“记录和回复”使用场景。其中一个工具是Mozilla rr调试器。大多数跟踪工具都能够使用时间戳标记事件,这使我们能够将其与那段时间内发生的外部事件关联。
5.3负载表征
负载表征是通过量化参数和函数来描述负载的过程,它的目标是定义负载的行为及主要特征。大体而言,应用程序分类为:交互式应用、数据库、网络应用、并行式应用。
5.3.1 统计性能事件
PMC通常有2种使用模式:计数和采样。计数模式用于负载表征,而采样模式用于寻找热点。计数背后的思想是统计程序运行期间某些性能事件的数量。perf stat工具用来统计各种硬件事件,如指令数、时钟周期数、缓存未命中等。
5.3.2 手动收集性能计数
现代CPU有几百个可统计的性能事件,理解何时使用特定的PMC就更难了。我们不推荐使用手动收集特定的PMC的原因在此。更建议使用Intel VTune Profiler来自动处理。
可以通过perf list查看可用的映射名称列表。
基于CPU的性能剖析工具在虚拟环境或者云环境中还是不能很好的运行。
5.3.3 事件多路复用和缩放
在某些情况下,我们想统计许多不同的事件,但是一个计数器一次只能统计一个事件,这就是为什么PMU中有多个计数器。自顶向下分析TMA在一次程序执行中需要收集多达100个不同的性能事件。
如果事件多与计数器,则分析工具使用时间多路复用技术使每个事件都有机会访问监控硬件。此时一个事件不会一直被测量,只有它的一部分能被测量。计算公式如下:
最终计数 = 原始计数 * (运行时间/启用时间)
多路复用技术和缩放技术可以安全地用于在长时间间隔内执行相同代码的稳定负载。如果程序经常在不同的热点之间跳转,就会出现盲点,在缩放过程中出现错误。为了避免缩放,可以尝试减少事件的数量使其不大于可用的物理PMC数量。
5.4 采样
采样能给出“代码中那个位置对某些性能事件的数量贡献最大?”的答案。
采样所收集的样本数据存储在数据收集文件中,这些可进一步用于显示调用图、程序中最耗时的部分和统计意义上重要的代码段控制流。
5.4.1 用户模式采样和基于硬件事件的采样
采样分为用户模式采样和基于硬件事件的采样EBS。用户采样是一种纯软件方法,它将代理库嵌入被分析的应用程序中。代理库为应用程序中每个线程设置OS计时器,在计时器计时完成时,应用程序会接收到SIGPROF信号,该信号由收集器处理。
用户模式采用只能识别热点,而EBS可涉及PMC的其它分析。EBS开销比用户模式小,收集频率更高,数据也更准确。
5.4.2 寻找热点
在准备工作(了解想采样内容)之后,PMC计数器每个时钟周期递增一次,最终它会溢出,然后硬件发起PMI。剖析工具会被配置成捕获PMI,一个中断服务例程来处理它们。
想知道热点列表中出现的每个函数中的热点代码段。要查看内联函数的剖析数据以及为特定源代码区域生成的汇编代码,需要在应用程序构建时带上调试信息-g。使用-gline-tables-only选项可以将采集的调试信息减少到源代码对应的符号行号。
5.4.3 采集调用栈
用Linux perf工具收集调用栈有3种方法:
1.帧指针perf record--call-graph fp。需要二进制文件在编译构建时带上--fnoomit-frame-pointer参数。
2. DWARF调试信息perf record--call-graph dwarf。需要二进制文件在编译构建时带上-g。
3. Intel最后分支记录(last branch record, LBR)硬件特性perf record--call-graph lbr。
了解调用栈的采集机制非常重要。开发者应当使用剖析工具来完成这项工作,它们可以更快、更准确地提供数据。
5.4.4 火焰图
火焰图是一种常用的可视化剖析数据和程序中最热代码路径的方法。
5.5 屋顶线性能模型
屋顶线性能模型是一种面向吞吐量的性能模型,常用于HPC。屋顶线表示应用程序不可能超过计算机处理能力的事实,程序中每个函数或者循环都收到计算机的计算或内存能力限制。
提高应用程序性能的传统方法是充分利用计算机的SIMD核多核功能,通常在向量化、内存和线程方面进行优化。在屋顶线图中,可以绘制标量单核、SIMD单核和SIMD多核性能的理论最大值,了解理论提升空间。
对于计算密集型而言,向量化和多线程。对于访存密集型,优化访问模式。
使用屋顶线模型优化性能的最终目标是将点向上移动,向量化和线程化将点上移,而通过增加算术强度优化内存访问将点右上移。
确定硬件限制后,根据屋顶线模型评估应用程序的性能。自动收集屋顶线模型数据最常用的2种方法是采样和二进制插桩。采样收集数据的开销较低,而二进制插桩可以通过更准确的结果。
总之,屋顶线性能模型有助于:
1. 识别性能瓶颈;
2. 指导软件优化;
3. 确定何时优化达到了极限;
4. 评估与机器能力相关的性能。
5.6 静态性能分析
对于C++而言,我们有诸如Clang Static Analyzer、Klocwork和Cppcheck等工具,它们旨在检测代码的正确性和语义。静态性能分析器不运行实际代码而是模拟代码运行,不过预测精度很难保证。
1. 不可能静态分析C++代码的性能,因为不知道它们被编译成什么机器码。因此,静态性能分析更适用于汇编代码。
2. 静态分析工具是模拟而不是执行,过程会很慢。所以选取一些汇编代码片段阐释预测,静态性能分析应用范围很窄。
静态分析工具的优点在于不需要真正的硬件就可以模拟不同CPU代系的代码,无需担心结果的一致性。缺点在于通常无法预测和模拟现代CPU中所有内容。
动态工具证明性能假设的唯一100%可靠的方法。缺点在于:
1. 需要访问特权,才能收集PMC之类底层性能数据;
2. 编写好的基准测试程序并衡量想要的指标并不总是那么容易;
3. 过滤噪声和各种副作用。
5.7 编译器优化报告
为了更好地与开发者进行交互,编译器提供了性能优化报告。如果我们想知道某个函数是否被内联,或者某个循环是否被向量化、展开?一种比较困难的分析办法是分析生成的汇编指令。但是,如果函数比较大时,这会特别困难阅读汇编指令。幸运的是,包括GCC\ICC和Clang在内的大多数编译器都提供了优化报告,供开发者检查编译器对特定代码片段做了那些优化。
编译器报告是按源文件生成的,输出的报告可能非常大。Complier Explorer网站有针对LLVM的编译器的“优化输出”工具,当鼠标悬停在代码行上,这些工具会报告执行过的转换。
编译器优化报告不仅有助于发现错过的优化机会,还可以解释发生这种情况的原因,而且对测试优化假设也很有用。编译器通常会根据其成本模型分析结果来确定某种转换是否有益,并通过#pragma、属性、编译器内建函数等提示编译器。
编译器优化报告应该成为你工具箱中关键工具之一,它能快速检查是否对特定热点代码进行了优化以及一些重要的优化是否失败了。
5.8 本章总结
1.时延和吞吐量通常是衡量性能的最终指标。
2. 代码插桩帮我们跟踪程序中许多内容,但有较大开销。
3. 跟踪有助于探索系统中异常。跟踪捕获完整的事件序列,且每个事件都附有时间戳。
4. 负载表征是一种根据运行时行为比较和分组应用程序的方法。
5. 采样会跳过程序执行的大部分时间,只取一个假定可以代表整个采样间隔的样本,但是它仍然能给出足够精确的样本分布。不需要重新编译,开销小,所以它是非常流行的性能分析方法。
6. 计数和采样会产生非常低的运行时开销(通常低于2%)。如果在不同事件之间进行多路复用,计数的开销会变得更高(5%~15%),而随着采样频率的增加,其开销也会更高。
7. 屋顶线模型是一种面向吞吐量的性能模型,有助于识别性能瓶颈,指导软件优化和跟踪优化进度。
8. 静态分析工具模拟代码片段。该方法有很多约束和限制,但是可以获得一份非常详细的底层分析报告。
9. 编译器优化报告有助于找到错过的编译器优化点,可以指导开发者构建新的性能实验。