赞
踩
性能分析的最终目标是确定瓶颈并找到与之相关联的代码位置。不幸的是,没有预定的步骤可以遵循,因此可以以许多不同的方式进行处理。
通常,对应用程序进行分析可以快速了解应用程序的热点。有时,这是开发人员需要做的一切来修复性能低效。尤其是高级性能问题通常可以通过分析揭示。例如,考虑这样一种情况,你使用感兴趣的特定函数对应用程序进行分析。根据你对应用程序的心理模型,你预计该函数是不经常使用的。但是,当你打开分析文件时,你会发现它消耗了大量的时间并且被调用了很多次。基于这些信息,你可以应用缓存或记忆化等技术来减少对该函数的调用次数,并期望看到显著的性能提升。
什么是缓存/记忆化技术:
缓存/记忆化技术是一种优化技术,用于提高计算机程序的性能。其基本思想是将计算的结果保存在内存中,以便在后续的计算中能够快速访问,从而避免重复地执行相同的计算。
缓存通常是通过将计算的结果存储在内存中的数据结构中实现的,这样可以更快地访问结果,而不需要重新计算。在某些情况下,可能需要使用特定的缓存算法来平衡存储空间和访问时间之间的权衡。
记忆化则是一种特殊的缓存技术,它通常涉及对递归函数进行优化。该技术通过将递归调用的结果存储在一个数据结构中,避免了多次进行相同的递归计算,从而大幅提高了递归函数的性能。
然而,当你已经修复了所有主要的性能低效问题,但仍需要从应用程序中挤出更多的性能时,仅仅知道特定函数的执行时间等基本信息是不够的。这时,你需要CPU提供的额外支持,以了解性能瓶颈所在的位置。因此,在使用本章节中提供的信息之前,请确保你正在尝试优化的应用程序没有严重的性能缺陷。因为如果存在这样的问题,使用CPU性能监控功能进行低级调整是没有意义的。它很可能会把你引向错误的方向,而且你只会浪费时间来调整糟糕的代码,而不是修复真正的高级性能问题。
个人经验:当我开始进行性能优化工作时,通常只是对应用程序进行分析并尝试通过基准测试中的热点来理解应用程序。这经常导致我进行无序的试验,如展开、矢量化、内联等操作。我并不是说这总是一种失败的策略。有时候你会幸运地从这些随机实验中获得巨大的性能提升。但通常情况下,你需要有很好的直觉和运气。
现代CPU不断推出新特性,以不同的方式增强性能分析。使用这些特性极大地简化了查找低级问题,如缓存未命中、分支预测错误等。在本章中,我们将介绍一些现代Intel CPU上可用的HW性能监控功能。其中大部分也存在于其他供应商的CPU(如AMD、ARM等)中。请查看相应部分以获取更多详细信息。
• Top-Down微架构分析方法(TMA)-一种强大的技术,用于识别程序对CPU微架构的无效使用。它表征了工作负载的瓶颈,并允许定位它发生的源代码的确切位置。它抽象了CPU微架构的复杂性,即使对于经验不足的开发人员也很容易使用。
• Last Branch Record(LBR)-一种机制,连续记录与执行程序并行的最近分支结果。它用于收集调用堆栈、识别热点分支、计算单个分支的误判率等。
• 处理器事件采样(PEBS)-一种增强采样的功能。其主要优点包括:降低采样开销和提供“精确事件”功能,允许确定导致特定性能事件的确切指令。
• Intel处理器跟踪(PT)-一种记录和重构每条指令执行时间戳的设施。它的主要用途是事后分析和查找性能故障。
上述功能从CPU角度提供了有关程序效率及如何使其更适合CPU的见解。性能分析工具利用它们提供许多不同类型的性能分析。
6.1 Top-Down Microarchitecture Analysis //TMA
TMA是一种非常强大的技术,用于识别程序中CPU的瓶颈。它是一个健壮和正式的方法,即使对于经验不足的开发人员也很容易使用。该方法最好的部分是它不需要开发人员对系统中的微架构和PMC有深入的了解,仍然能够有效地找到CPU的瓶颈。但是,它并不会自动解决问题;否则,这本书也不会出现。
Figure 28: The concept behind TMA’s top-level breakdown. © Image from [Yasin, 2014]
在高层次上,TMA确定了程序中每个热点执行所遇到的障碍。瓶颈可能与四个组件之一有关:前端瓶颈,后端瓶颈,退役和错误预测。图28阐明了这个概念。以下是如何阅读此图表的简短指南。从第3节中我们所知道的,CPU中有内部缓冲区来跟踪正在执行的指令的信息。每当新指令被提取和解码时,将分配这些缓冲区的新条目。如果某个指令的uop在特定的执行周期内未被分配,原因可能是我们无法提取和解码它(前端瓶颈),或者后端被过载且无法分配新的uop工作和资源(后端瓶颈)。已分配并安排执行但未退役的uop与错误预测桶有关。这样的uop的一个示例可以是某个被推测执行但后来被证明是在错误的程序路径上,没有退役的指令。最后,退役是我们希望所有uop都达到的目标,尽管也有例外情况。对于非矢量化代码来说,高退役值可能是用户将代码矢量化的好提示(请参见第8.2.3节)。在程序操作denormal浮点值使得此类操作非常缓慢的情况下,我们可能会看到高退役值但总体性能较慢的情况(请参见第10.4节)。
Figure 29: The TMA hierarchy of performance bottlenecks. © Image by Ahmad Yasin.
图28针对程序中的每个指令进行了详细的分析。然而,分析工作负载中的每个单独指令肯定是过度的,当然,TMA也不会这样做。相反,我们通常对整个程序被阻塞的原因感兴趣。为了实现这个目标,TMA通过收集特定的指标(PMC的比率)来观察程序的执行。基于这些指标,它将应用程序与四个高级桶之一相关联。每个高级桶都有嵌套类别(参见图29),可以更好地分解程序中的CPU性能瓶颈。我们多次运行工作负载,每次关注特定的指标并逐层分析,直到得出更详细的性能瓶颈分类。例如,最初,我们收集四个主要桶的指标:前端瓶颈、后端瓶颈、退役、错误预测。假设我们发现程序执行的大部分被内存访问阻塞了(这是一个后端瓶颈,见图29)。下一步是再次运行工作负载,并仅收集与Memory Bound桶相关的指标(逐层分析)。这个过程重复进行,直到我们知道确切的根本原因,例如L3 Bound。
non-stalled stalled解释下:
在计算机领域,非停顿(non-stalled)是指CPU正在执行指令的状态,没有被任何阻止因素所影响。而停顿(stalled)则是指CPU在执行中遇到了某些阻止因素,无法继续前进,需要等待某些条件满足后才能继续执行。CPU停顿时会浪费宝贵的时间,导致程序执行速度变慢。
在现实世界的应用中,性能可能会受到多个因素的限制。例如,程序可能同时遇到大量分支预测错误(Bad Speculation)和缓存未命中(Back End Bound)等问题。在这种情况下,TMA将同时对多个桶进行逐层分析,并确定每种性能瓶颈对程序性能的影响。像Intel VTune Profiler、AMD uprof和Linux perf等分析工具可以在单次运行基准测试时计算出所有指标。
TMA指标的前两级以可用于程序执行的所有管道插槽的百分比表示(见第4.5节)。它允许TMA提供CPU微架构利用率的准确表示,考虑到处理器的全部带宽。
在确定了程序的性能瓶颈后,我们会想知道它具体发生在代码的哪一行。TMA的第二阶段是跟踪性能问题的源头,直到精确定位到代码的确切行和汇编指令。分析方法提供了准确的PMC,供每类性能问题使用。然后,开发人员可以使用该PMC来查找在由第一阶段确定的最关键性能瓶颈所在的源代码区域。这种对应关系可以在“Locate-with”列的TMA指标表中找到。例如,在Intel Skylake处理器上运行的应用程序中定位与高DRAM_Bound指标相关的性能瓶颈,应该在MEM_LOAD_RETIRED.L3_MISS_PS性能事件上进行采样。
//下面有些还是没分清,先记录着
用simpleperf分析CPU Backend Bound,可以采样哪些事件?
在使用Simpleperf分析CPU后端瓶颈时,可以采样以下事件:
1. cpu-cycles:记录CPU时钟周期,这是一种通用的事件,可以用于衡量CPU活跃度。
2. instructions:记录已经执行的指令数,可以与CPU周期事件一起使用来计算每个指令的平均周期数。
3. cache-references:记录对缓存的所有引用操作数量。
4. cache-misses:记录未能从缓存中找到数据而导致的缓存访问错误的数量。
5. branch-misses:记录分支预测错误的数量。
6. page-faults:记录由于缺失页表项而导致的缺页故障数量。
7. context-switches:记录进程上下文切换的数量。
以上这些事件可以帮助开发者分析程序CPU Backend Bound。例如,通过分析 cache-misses 事件,可以确定程序中哪些部分频繁地出现缓存未命中,导致 CPU 等待缓存访问而降低了性能。开发者可以根据这些信息来优化程序和代码结构,提高程序性能。
用simpleperf分析CPU Fronted Bound,可以采样哪些事件?
在使用Simpleperf分析CPU Frontend Bound时,可以采样以下事件:
1. instructions: 记录已经执行的指令数,用于衡量CPU前端活跃度。
2. branch-instructions: 记录所有分支指令的执行次数。
3. branch-misses: 记录因为分支预测错误而导致的不必要指令流失处理器流水线的次数。
4. L1-icache-load-misses: 记录由于读取L1指令缓存未命中而导致的L1缓存加载开销。
5. front-end-stalls: 记录因为CPU前端问题而导致的CPU空转或停顿的时间。
以上这些事件可以帮助开发者分析程序的 CPU 前端瓶颈。例如,通过分析 branch-misses 事件,可以确定程序中哪些部分会经常导致分支预测错误,从而导致CPU浪费时间等待正确的代码路径。开发者可以根据这些信息来优化程序和代码结构,以提高性能并降低CPU前端瓶颈。
用simpleperf分析CPU Bad Speculation,可以采样哪些事件?
在使用Simpleperf分析 CPU Bad Speculation 时,可以采样以下事件:
1. branch-misses:记录分支预测错误的数量,这是最常用的 Bad Speculation 事件。
2. misalignment-faults:记录由于内存地址对齐问题导致的缺陷故障(Alignment Fault)的数量。
3. bus-cycles:记录由于总线竞争而浪费的时钟周期数。
4. stalled-cycles-frontend:记录因为CPU前端瓶颈而导致的CPU空转或停顿的时钟周期数。
5. stalled-cycles-backend:记录因为CPU后端瓶颈而导致的CPU空转或停顿的时钟周期数。
6. cycles:记录CPU时钟周期数,这是一个通用的事件,可以用于衡量程序执行时间及CPU活跃度。
以上这些事件可以帮助开发者分析CPU Bad Speculation。例如,通过分析 branch-misses 事件,可以确定程序中哪些部分经常出现分支预测错误,从而导致性能损失。开发者可以根据这些信息来优化程序和代码结构,以减少 Bad Speculation 事件并提升程序性能。
用simpleperf分析CPU Retiring,可以采样哪些事件?
在使用Simpleperf分析 CPU Retiring 时,可以采样以下事件:
1. instructions:记录已经执行的指令数,用于衡量CPU效率。
2. cpu-cycles:记录CPU时钟周期数,这是一个通用的事件,可以用于衡量CPU活跃度。
3. branches:记录已经执行的所有分支指令数目。
4. branch-misses:记录因为分支预测错误而导致的不必要指令流失处理器流水线的次数。
5. stalled-cycles-backend: 记录因为后端瓶颈而导致的CPU空转或停顿的时钟周期数。
6. retired-instructions:记录已经从CPU中成功退役(Retire)并完成处理的指令数目。
以上这些事件可以帮助开发者分析程序的CPU Retiring情况。例如,通过分析 retired-instructions 事件,可以确定程序中哪些指令顺利完成了退役,从而优化应用程序的效率和性能。开发者可以根据这些信息来优化程序和代码结构,以提高Retiring情况并提升程序性能。
6.1.1 TMA in Intel® VTune™ Profifiler
Figure 30: Intel VTune Profifiler “Microarchitecture Exploration” analysis.
Figure 31: “Microarchitecture Exploration” Bottom-up view.
Figure 32: “Microarchitecture Exploration” source code and assembly view.
TMA可通过最新的Intel VTune Profiler中的“微架构探索”分析功能进行特征分析。图30显示了7-zip基准测试的分析摘要。从图表中可以看出,由于CPU Bad Speculation,特别是误预测的分支,导致了大量的执行时间浪费。
这个工具的美妙之处在于,您可以点击您感兴趣的指标,工具将带您到显示对该特定指标做出贡献的顶级函数的页面。例如,如果您点击Bad Speculation指标,您将看到类似于图31所示的内容。
从那里,如果您双击LzmaDec_DecodeReal2函数,Intel® VTune™ Profiler将带您进入源代码级别视图,如图32所示。突出显示的行导致了LzmaDec_DecodeReal2函数中分支预测错误数量最多。
6.1.2 TMA in Linux Perf //重点
从Linux内核4.8开始,perf有一个选项--topdown,用于在perf stat命令中打印TMA一级指标,即只有四个高级桶:
- $ perf stat --topdown -a -- taskset -c 0 ./7zip-benchmark b
- retiring bad speculat FE bound BE bound
- S0-C0 30.8% 41.8% 8.8% 18.6% <==
- S0-C1 17.4% 2.3% 12.0% 68.2%
- S0-C2 10.1% 5.8% 32.5% 51.6%
- S0-C3 47.3% 0.3% 2.9% 49.6%
- ...
simpleperf中有类似perf中--topdown的指令么?
是的,simpleperf 中也有类似于 perf 的 --topdown 选项。通过在 simpleperf stat 命令中使用 --sort comm,dso,topdown 选项,可以按照 TMA 的层次结构进行排序,输出针对每个区段的时间、CPU 循环数、Retired 指令、缓存失效等信息,提供了一种快速分析程序性能的工具。该选项可用于快速查看和诊断程序中的性能瓶颈,并确定哪些函数最大程度地影响了程序的整体性能。
为了获取高级的 TMA 指标值,Linux perf 需要对整个系统进行剖析(-a 选项)。这就是为什么我们会看到所有核心的度量值。但是,由于我们使用 taskset -c 0 将基准测试固定在 core0 上,因此只需关注与 S0-C0 对应的第一行即可。
要获取 Top-Down 的二级、三级等指标,可以使用 toplev 工具。toplev 工具是 pmu-tools 中的一部分,由 Andi Kleen 编写,它是用 Python 实现的,底层调用 Linux perf。您将在下一节中看到使用它的示例。必须启用特定的 Linux 内核设置才能使用 toplev,请查看文档以获取更多详细信息。为了更好地呈现工作流程,下一节提供了一个逐步说明如何使用 TMA 来改善面向内存的应用程序性能的示例。
个人经验: Intel® VTune™ Profiler 是一种非常强大的工具,毫无疑问。然而,对于快速实验,我经常使用 Linux perf,因为它在我工作的每个 Linux 发行版上都可用。因此,下一节中的示例使用 Linux perf 进行探索。
https://github.com/andikleen/pmu-tools/wiki/toplev-manual
https://github.com/andikleen/pmu-tools
//整体思路是根据toplev先判断出CPU中哪个部分出现问题比较大,然后找出这个部分对应的perf cmd,然后分析汇编和源代码,最后优化code!!!
//toplev是python写的,手机上可以运行么?
//用simpleperf分析Memory Bound,可以采样哪些事件?
在使用 simpleperf 分析面向内存的应用程序时,可以采样以下事件:
- CPU 循环计数(CPU cycles)
- 缓存失效指令(cache-misses)
- 指令执行总数(instructions)
- L1 缓存命中(L1-dcache-loads-misses)
- L2 缓存命中(L1-dcache-loads-misses,L2-dcache-loads-misses)
以上事件可提供有关程序在内存中读取和写入数据时发生的系统事件的信息。通过收集这些事件的指标,可以确定性能瓶颈和优化机会。选择哪些事件采样取决于特定的应用程序场景和目标。在进行采样之前,建议先进行实验和基准测试,以确定应用程序的性能瓶颈和最佳优化策略。
//Intel CPU的perf MEM_LOAD_RETIRED.L3_MISS_PS指令,ARM CPU上simpleperf的指令是什么?
在 ARM CPU 上,可以使用 simpleperf 工具采样 L3 缓存未命中事件,对应的指令是 `mem-loads,l3-cache-misses`。该指令旨在计算所有内存加载操作且 L3 缓存未命中的次数,并可以帮助分析面向内存的应用程序的性能瓶颈。
以下是针对 ARM CPU 的示例命令:
- ```
- sudo simpleperf stat -e mem-loads,l3-cache-misses program
- ```
这将对 `program` 应用程序进行采样,并显示包括 L3 缓存未命中事件在内的指定事件的计数、占总时间的百分比等信息。需要注意的是,`mem-loads,l3-cache-misses` 指令可能不适用于所有 ARM CPU 架构,具体情况需要根据特定的硬件和系统配置来判断。
6.1.3 Step1: Identify the bottleneck
假设我们有一个小型的基准测试程序(a.out),运行时间为 8.5 秒。这个基准测试程序的完整源代码可以在 GitHub 上找到。
- $ time -p ./a.out
- real 8.53
作为第一步,我们运行我们的应用程序并收集特定的度量指标,以帮助我们对它进行表征,即我们试图检测我们的应用程序属于哪个类别。以下是我们基准测试程序的一级度量指标:
- $ ~/pmu-tools/toplev.py --core S0-C0 -l1 -v --no-desc taskset -c 0 ./a.out
- ...
- # Level 1
- S0-C0 Frontend_Bound: 13.81 % Slots
- S0-C0 Bad_Speculation: 0.22 % Slots
- S0-C0 Backend_Bound: 53.43 % Slots <==
- S0-C0 Retiring: 32.53 % Slots
请注意,该进程被固定到 CPU0 上(使用 `taskset -c 0` 命令),并且 toplev 的输出仅限于此核心(`--core S0-C0`)。通过查看输出,我们可以判断应用程序的性能受到 CPU 后端的限制。而现在不需要分析它,让我们深入了解一级细节:
- $ ~/pmu-tools/toplev.py --core S0-C0 -l2 -v --no-desc taskset -c 0 ./a.out
- ...
- # Level 1
- S0-C0 Frontend_Bound: 13.92 % Slots
- S0-C0 Bad_Speculation: 0.23 % Slots
- S0-C0 Backend_Bound: 53.39 % Slots
- S0-C0 Retiring: 32.49 % Slots
- # Level 2
- S0-C0 Frontend_Bound.FE_Latency: 12.11 % Slots
- S0-C0 Frontend_Bound.FE_Bandwidth: 1.84 % Slots
- S0-C0 Bad_Speculation.Branch_Mispred: 0.22 % Slots
- S0-C0 Bad_Speculation.Machine_Clears: 0.01 % Slots
- S0-C0 Backend_Bound.Memory_Bound: 44.59 % Slots <==
- S0-C0 Backend_Bound.Core_Bound: 8.80 % Slots
- S0-C0 Retiring.Base: 24.83 % Slots
- S0-C0 Retiring.Microcode_Sequencer: 7.65 % Slots
我们看到应用程序的性能受到内存访问的限制。将近一半的 CPU 执行资源被浪费在等待内存请求完成上。现在让我们深入挖掘一层:
- $ ~/pmu-tools/toplev.py --core S0-C0 -l3 -v --no-desc taskset -c 0 ./a.out
- ...
- # Level 1
- S0-C0 Frontend_Bound: 13.91 % Slots
- S0-C0 Bad_Speculation: 0.24 % Slots
- S0-C0 Backend_Bound: 53.36 % Slots
- S0-C0 Retiring: 32.41 % Slots
- # Level 2
- S0-C0 FE_Bound.FE_Latency: 12.10 % Slots
- S0-C0 FE_Bound.FE_Bandwidth: 1.85 % Slots
- S0-C0 BE_Bound.Memory_Bound: 44.58 % Slots
- S0-C0 BE_Bound.Core_Bound: 8.78 % Slots
- # Level 3
- S0-C0-T0 BE_Bound.Mem_Bound.L1_Bound: 4.39 % Stalls
- S0-C0-T0 BE_Bound.Mem_Bound.L2_Bound: 2.42 % Stalls
- S0-C0-T0 BE_Bound.Mem_Bound.L3_Bound: 5.75 % Stalls
- S0-C0-T0 BE_Bound.Mem_Bound.DRAM_Bound: 47.11 % Stalls <==
- S0-C0-T0 BE_Bound.Mem_Bound.Store_Bound: 0.69 % Stalls
- S0-C0-T0 BE_Bound.Core_Bound.Divider: 8.56 % Clocks
- S0-C0-T0 BE_Bound.Core_Bound.Ports_Util: 11.31 % Clocks
我们发现瓶颈在 DRAM_Bound 中。这告诉我们许多内存访问在所有缓存层次上都未命中并且直接到达主存储器。如果我们收集了该程序的 L3 缓存未命中(DRAM 命中)的绝对数,也可以加以确认。对于 Skylake 架构,DRAM_Bound 指标使用 CYCLE_ACTIVITY.STALLS_L3_MISS 性能事件进行计算。让我们来收集它:
- $ perf stat -e cycles,cycle_activity.stalls_l3_miss -- ./a.out
- 32226253316 cycles
- 19764641315 cycle_activity.stalls_l3_miss
根据 CYCLE_ACTIVITY.STALLS_L3_MISS 的定义,它计算的是当执行被卡住时(即 L3 缓存未命中且尚未完成加载操作)发生的周期数。我们可以看到大约有 60% 的这种周期,这很糟糕。
6.1.4 Step2: Locate the place in the code
作为 TMA 过程的第二步,我们将定位代码中瓶颈最频繁出现的位置。为此,我们应该使用与在第 1 步中确定的瓶颈类型相对应的性能事件对工作负载进行采样。建议找到这种事件的一种方法是使用 toplev 工具并使用 --show-sample 选项,这将提供可用于定位问题的 perf record 命令行。为了理解 TMA 的机制,我们还提供了手动找到与特定性能瓶颈相关联的事件的方法。在本章早些时候介绍的 TMA metrics120 表中,性能瓶颈和应该用于定位此类瓶颈发生位置的性能事件之间的对应关系可以得到。 Locate-with 列指示应该使用哪种性能事件来定位代码中确切的问题出现位置。对于我们的例子,在找到导致 DRAM_Bound 指标(L3 缓存未命中)值过高的内存访问时,应使用 MEM_LOAD_RETIRED.L3_MISS_PS 精确事件进行采样,如上面的清单所示:
- $ perf record -e cpu/event=0xd1,umask=0x20,name=MEM_LOAD_RETIRED.L3_MISS/ppp
- ./a.out
- $ perf report -n --stdio
- ...
- # Samples: 33K of event ‘MEM_LOAD_RETIRED.’L3_MISS
- # Event count (approx.): 71363893
- # Overhead
- Samples
- Shared Object
- Symbol
- # ........
- ......... .................
- .................
- #
- 99.95% 33811 a.out [.] foo
- 0.03% 52 [kernel] [k] get_page_from_freelist
- 0.01% 3 [kernel] [k] free_pages_prepare
- 0.00% 1 [kernel] [k] free_pcppages_bulk
大多数 L3 未命中是由可执行文件 a.out 中的函数 foo 中的内存访问引起的。为避免编译器优化,函数 foo 采用汇编语言实现,如清单11所示。基准测试的“驱动程序”部分在主函数中实现,如清单12所示。我们分配了足够大的数组 a,以使其不适合 L3 缓存。基准测试基本上生成一个随机索引到数组 a,并将其与数组 a 的地址一起传递给 foo 函数。稍后,foo 函数会读取这个随机的内存位置。
Listing 11 Assembly code of function foo.
- $ perf annotate --stdio -M intel foo
- Percent | Disassembly of a.out for MEM_LOAD_RETIRED.L3_MISS
- ------------------------------------------------------------
- : Disassembly of section .text:
- :
- : 0000000000400a00 <foo>:
- : foo():
- 0.00 : 400a00: nop DWORD PTR [rax+rax*1+0x0]
- 0.00 : 400a08: nop DWORD PTR [rax+rax*1+0x0]
- ...
- 100.00 : 400e07: mov rax,QWORD PTR [rdi+rsi*1] <==
- ...
- 0.00 : 400e13: xor rax,rax
- 0.00 : 400e16: ret
Listing 12 Source code of function main.
- extern "C" { void foo(char* a, int n); }
- const int _200MB = 1024*1024*200;
- int main() {
- char* a = (char*)malloc(_200MB); // 200 MB buffer
- ...
- for (int i = 0; i < 100000000; i++) {
- int random_int = distribution(generator);
- foo(a, random_int);
- }
- ...
- }
从清单11中可以看出,函数 foo 中的所有 L3 缓存未命中都标记为单个指令。既然我们知道哪个指令引起了这么多 L3 未命中,让我们来修复它。
6.1.5 Step3: Fix the issue
因为在获取下一个将要访问的地址和实际读取指令之间存在一个时间窗口,所以我们可以添加预取提示,如清单13所示。有关内存预取的更多信息,请参见第8.1.2节。这个提示将执行时间提高了2秒,速度提高了30%。请注意,在 CYCLE_ACTIVITY.STALLS_L3_MISS 事件中的值少了10倍:
Listing 13 Inserting memory prefetch into main.
- for (int i = 0; i < 100000000; i++) {
- int random_int = distribution(generator);
- + __builtin_prefetch ( a + random_int, 0, 1);
- foo(a, random_int);
- }
- $ perf stat -e cycles,cycle_activity.stalls_l3_miss -- ./a.out
- 24621931288 cycles
- 2069238765 cycle_activity.stalls_l3_miss
- 6,498080824 seconds time elapsed
TMA 是一个迭代过程,因此现在我们需要从步骤1开始重复该过程。可能会将瓶颈移动到其他 bucket 中,在本例中是 Retiring。这是一个演示 TMA 方法工作流程的简单示例。分析实际应用程序不太可能那么容易。本书的下一个完整章节是按照方便与TMA过程一起使用的方式组织的(7~11章节)。例如,其各个部分分解为反映每个高级别性能瓶颈类别的内容。这种结构背后的想法是提供某种检查列表,开发人员可以在发现性能问题后使用它来推动代码更改。例如,当开发人员发现他们正在处理的应用程序是内存限制时,他们可以参考第8.1节中的想法。
6.1.6 Summary
Figure 33: Top Level TMA metrics for SPEC CPU2006. © Image by Ahmad Yasin, http:// cs.hai fa.ac.il/~yosi/PARC/ yasin.pdf .
TMA 对于识别代码中的 CPU 性能瓶颈非常有效。理想情况下,当我们在某个应用程序上运行它时,希望看到 Retiring 指标达到 100%。这意味着该应用程序完全饱和了 CPU。在玩具程序上可以接近这个结果。然而,在现实世界中,实现这个目标还有很长的路要走。图33显示了Skylake CPU代的SPEC CPU2006124基准测试的顶级TMA指标。请记住,由于架构师不断努力改进 CPU 设计,这些数字可能会因其他 CPU 代变化。这些数字也可能会因其他指令集架构(ISA)和编译器版本的变化而发生变化。
不建议在具有重大性能缺陷的代码上使用 TMA,因为这可能会引导您走向错误的方向,而不是解决真正的高级性能问题,您将调整不好的代码,这只是浪费时间。同样,确保环境不会干扰分析。例如,如果您删除文件系统缓存并在 TMA 下运行基准测试,它可能会显示应用程序受到内存限制,而实际上,当文件系统缓存已经取暖时,可能是错误的。
由 TMA 提供的工作负载特征化可以将潜在优化的范围扩展到源代码以外。例如,如果应用程序受到内存限制,并且已经检查了所有可能的软件水平加速方式,则可以通过使用更快的内存来改进内存子系统。这样可以进行有教育意义的实验,因为只有在发现程序受到内存限制并且将从更快的内存中受益时,才会花钱。
在撰写本文时,AMD 处理器的 TMA 指标的第一级也可以使用。
Additional resources and links:
这是一些关于性能分析和计数器体系结构的顶部方法的参考资料:
• Ahmad Yasin 的论文“一种自上而下的性能分析和计数器体系结构方法”[Yasin,2014]。
• Ahmad Yasin 在 IDF'15 上的演讲“通过 Skylake 上的自上而下分析简化软件优化”,URL: https://youtu.be/kjufVhyuV_A。
• Andi Kleen 的博客 - pmu-tools,第二部分:toplev,URL: http://halobates.de/blog/p/262。
• toplev 手册,URL: https://github.com/andikleen/pmu-tools/wiki/toplev-manual。
• 了解 Intel® VTune™ Profiler 中通用探索如何工作,URL: https://software.intel.com/en-us/articles/understanding-how-general-exploration-works-in-intel-vtune-amplifier-xe。
6.2 Last Branch Record //最后分支记录
现代的 Intel 和 AMD CPU 都具有称为最后分支记录(LBR)的功能,在其中CPU会持续记录许多先前执行的分支。但在深入了解详情之前,人们可能会问:为什么我们如此关注分支?这是因为这是我们能够确定程序控制流的方式。在基本块中,我们主要忽略其他指令(请参见第7.2节),因为分支总是基本块中的最后一个指令。由于基本块中的所有指令都保证至少被执行一次,因此我们只能关注“代表”整个基本块的分支。因此,如果我们跟踪每个分支的结果,就可以重构程序的整个逐行执行路径。实际上,这就是 Intel 处理器跟踪(PT)功能能够执行的操作,这将在第6.4节中讨论。 LBR 功能早于 PT,并具有不同的用例和特殊功能。
Figure 34: 64-bit Address Layout of LBR MSR. © Image from [Int, 2020].
由于 LBR 机制,CPU 可以在执行程序的同时不影响性能地持续记录分支到一组特定模型的寄存器(MSR),从而引起最小的减速。硬件会记录每个分支的“来自”和“去往”地址以及一些附加的元数据(见图34)。这些寄存器就像一个环形缓冲区,不断被覆盖,并且只提供最近的32个分支结果。如果我们收集足够长的源目标对的历史记录,我们就可以像有限深度的调用堆栈一样展开程序的控制流程。
有了 LBR,我们可以对分支进行采样,但在每个样本中,要查看已执行的 LBR 堆栈中的先前分支。这在热代码路径中可以提供合理的控制流覆盖,但不会给我们带来太多信息,因为只检查了总分支的较小的数量。需要记住的重要一点是,这仍然是采样,因此不能检查每个执行的分支。 CPU通常执行速度太快,无法进行检查。[Kleen, 2016]
• 最后分支记录(LBR)堆栈-由于 Skylake 提供了32对存储最近执行分支的源和目标地址的 MSR。
• 最后分支记录堆栈顶部(TOS)指针-包含指向包含最近记录的分支、中断或异常的 LBR 栈中的 MSR 的指针。
需要非常注意的是,只有被执行的分支才会使用 LBR 机制记录下来。下面是一个示例,展示了如何在 LBR 堆栈中跟踪分支结果。
以下是我们在执行 CALL 指令时所期望在 LBR 堆栈中看到的内容。由于 JNS 分支(4eda14 -> 4eda1e)没有被执行,因此没有被记录下来,因此不会出现在 LBR 堆栈中:
- FROM_IP TO_IP
- ... ...
- 4eda2d 4eda10
- 4eda1e 4edb26 <== LBR TOS
个人经验:未采取的分支未被记录可能会增加一些额外的分析负担,但通常不会使其过于复杂。我们仍然可以展开 LBR 堆栈,因为我们知道控制流是从 TO_IP(N-1)顺序执行到 FROM_IP(N) 的。
从 Haswell 开始,LBR 输入增加了用于检测分支错误预测的组件。LBR 记录中有一个专用位用于它(请参见[Int, 2020,卷3B,第17章])。自 Skylake 起,还向 LBR 记录中添加了额外的 LBR_INFO 组件,其中包含 Cycle Count 字段,该字段计算自上一次更新 LBR 堆栈以来经过的核心时钟数。这些增加的组件有重要的应用,我们将在后面进行讨论。特定处理器的 LBR 记录的确切格式可以在 [Int,2020,卷3B,第17,18章] 中找到。
用户可以通过执行以下命令来确保其系统启用了 LBR:
- $ dmesg | grep -i lbr
- [ 0.228149] Performance Events: PEBS fmt3+, 32-deep LBR, Skylake events,
- full-width counters, Intel PMU driver.
6.2.1 Collecting LBR stacks
使用 Linux perf,可以使用以下命令收集 LBR 堆栈:
- $ ~/perf record -b -e cycles ./a.exe
- [ perf record: Woken up 68 times to write data ]
- [ perf record: Captured and wrote 17.205 MB perf.data (22089 samples) ]
使用perf record --call-graph lbr命令也可以收集LBR堆栈,但所收集的信息量比使用perf record -b要少。例如,当运行perf record --call-graph lbr时,不会收集分支预测失误和周期数据。由于每个收集的样本都捕获了整个LBR堆栈(32个最新的分支记录),因此所收集的数据(perf.data)的大小要比不使用LBR采样要大得多。下面是Linux perf命令,可以用来转储所收集的分支堆栈的内容:
$ perf script -F brstack &> dump.txt
如果我们查看dump.txt文件的内容(它可能很大),我们将看到类似于下面所示的东西:
- ...
- 0x4edabd/0x4edad0/P/-/-/2 0x4edaf9/0x4edab0/P/-/-/29
- 0x4edabd/0x4edad0/P/-/-/2 0x4edb24/0x4edab0/P/-/-/23
- 0x4edadd/0x4edb00/M/-/-/4 0x4edabd/0x4edad0/P/-/-/2
- 0x4edb24/0x4edab0/P/-/-/24 0x4edadd/0x4edb00/M/-/-/4
- 0x4edabd/0x4edad0/P/-/-/2 0x4edb24/0x4edab0/P/-/-/23
- 0x4edadd/0x4edb00/M/-/-/1 0x4edabd/0x4edad0/P/-/-/1
- 0x4edb24/0x4edab0/P/-/-/3 0x4edadd/0x4edb00/P/-/-/1
- 0x4edabd/0x4edad0/P/-/-/1 0x4edb24/0x4edab0/P/-/-/3
- ...
在上面的文本块中,我们展示了LBR堆栈的八个条目,该堆栈通常由32个LBR条目组成。每个条目都有FROM和TO地址(十六进制值)、预测标志(M/P)127以及一定数量的周期数(每个条目的最后一个位置上的数字)。带有“-”标记的组件与事务性内存(TSX)相关,这里我们不会讨论。感兴趣的读者可以查阅perf脚本规范中解码LBR条目的格式。
使用LBR有许多重要的用途。在接下来的章节中,我们将介绍其中最重要的几个。
6.2.2 Capture call graph //捕获调用图
在5.4.3节中,已经讨论了收集调用图及其重要性。即使您使用编译器选项-fomit-frame-pointer(默认为ON)或没有调试信息,也可以使用LBR来收集调用图信息:
-fomit-frame-pointer是GCC的编译选项之一,该选项可以让编译器在生成可执行文件时省略掉函数调用过程中的帧指针。这样可以减小可执行文件的大小并提高程序的运行效率,但是这也会影响到程序的调试。
正如你所看到的,我们确定了程序中最热门的函数(即bar函数)。此外,我们还发现了调用bar函数所耗费时间最多的调用者(即foo函数)。在这种情况下,我们可以看到在bar函数中有91%的样本与其调用函数foo相关。使用LBR特性,我们可以确定一个超级块(Hyper Block,有时也称为超级块),它是整个程序中执行最频繁的基本块链。该链中的基本块不一定按顺序排列,但它们是按顺序执行的。
6.2.3 Identify hot branches
LBR特性还可以让我们知道哪些分支被最频繁地执行:
从这个例子中,我们可以看到超过50%的分支在bar函数内,22%的分支是来自于foo到bar的函数调用等等。请注意,perf从周期事件切换到分析LBR堆栈:仅收集了670个样本,但是我们有一个完整的LBR堆栈与每个样本一起捕获。这为我们提供了670 * 32 = 21440个LBR条目(分支结果)进行分析。大多数情况下,我们可以根据代码行和目标符号确定分支的位置。然而,理论上,代码可能会有两个if语句写在同一行上。此外,在扩展宏定义时,所有扩展的代码都具有相同的源行,这也是可能发生这种情况的另一种情况。这个问题并不完全阻碍分析,但只会使其变得更加困难。为了消除两个分支之间的歧义,您可能需要自己分析原始的LBR堆栈(请参见easyperf博客上的示例)。
6.2.4 Analyze branch misprediction rate
还可以知道热门分支的预测错误率:
在这个例子中,与LzmaDec函数对应的行特别引起我们的兴趣。使用第6.2.3节的推理,我们可以得出源代码行dec.c:36上的分支是基准测试中执行最多的分支。在Linux Perf提供的输出中,我们可以发现两个与LzmaDec函数相对应的条目:一个带有Y字母,一个带有N字母。分析这两个条目可以给我们提供分支的预测错误率。在本例中,我们知道dec.c:36行上的分支被预测了303391次(对应于N),其中41665次预测错误(对应于Y),这给我们提供了88%的预测率。
Linux Perf通过分析每个LBR条目并从中提取预测错误位来计算预测错误率。因此,对于每个分支,我们都有一个正确预测的次数和错误预测的次数。同样,由于采样的性质,有些分支可能具有N条目但没有相应的Y条目。这可能意味着该分支没有被预测错误的LBR条目,但并不一定意味着预测率等于100%。
6.2.5 Precise timing of machine code //机器码的精确计时
正如第6.2节所述,从Skylake架构开始,LBR条目具有Cycle Count信息。该附加字段为我们提供了在两个已采取的分支之间经过的周期数。如果前一个LBR条目中的目标地址是某个基本块(BB)的开头,并且当前LBR条目的源地址是相同基本块的最后一条指令,则循环计数是此基本块的延迟时间。例如:
- 400618: movb $0x0, (%rbp,%rdx,1) <= start of a BB
- 40061d: add $0x1, %rdx
- 400621: cmp $0xc800000, %rdx
- 400628: jnz 0x400644 <= end of a BB
假设我们在LBR堆栈中有两个条目:
- FROM_IP TO_IP Cycle Count
- ... ... ...
- 40060a 400618 10
- 400628 400644 5 <== LBR TOS
Figure 35: Probability density function for latency of the basic block that starts at address 0x400618.
根据这些信息,我们知道基本块从偏移量400618开始执行时有一次在5个周期内完成的情况。如果我们收集足够的样本,可以绘制该基本块延迟时间的概率密度函数(见图35)。此图表是通过分析满足上述规则的所有LBR条目编制的。例如,基本块只在约75个周期内执行4%的时间,但更常见的是在260到314个周期之间执行。该块中有一个随机的数组加载,该数组无法适配CPU L3缓存,因此基本块的延迟很大程度上取决于此加载。图35中显示了两个重要峰值:第一个峰值约为80个周期,对应于L3缓存命中,而第二个峰值约为300个周期,对应于L3缓存未命中,其中加载请求一直到达主存储器。
这些信息可以用于进一步微调此基本块。例如,可以使用内存预取技术来优化此示例,我们将在第8.1.2节中讨论。此外,这些周期信息可以用于计时循环迭代,其中每个循环迭代均以已采取的分支(反向边)结束。
关于如何为任意基本块构建延迟时间的概率密度函数的示例可以在easyperf博客中找到。但是,在较新版本的Linux perf中,获取此信息要容易得多。
为了使输出适合页面,删除了几行不重要的信息。现在,如果我们专注于源和目标是dec.c:174的分支,则可以找到与之关联的多个行。Linux perf首先按开销对条目进行排序,这需要我们手动过滤我们感兴趣的分支的条目。实际上,如果我们过滤它们,就会得到以该分支结束的基本块的延迟时间分布,如表5所示。稍后用户可以绘制此数据并获得类似于图35的图表。
目前,定时LBR是系统中最精确的按周期计时信息源。
6.2.6 Estimating branch outcome probability //估计分支结果概率
稍后在第7节中,我们将讨论代码布局对性能的重要性。更进一步地说,以穿过方式提供热路径通常会提高程序的性能。考虑单个分支,知道该条件99%的时间是假还是真对编译器做出更好的优化决策至关重要。LBR功能使我们能够在不插桩代码的情况下获得此数据。作为分析结果,用户将获得条件真实和假的结果之间的比率,即分支被执行的次数和未执行的次数。此功能在分析间接跳转(switch语句)和间接调用(虚拟调用)时特别有用。可以在easyperf博客上找到在真实应用程序中使用它的示例。
6.2.7 Other use cases
• 面向性能的配置。LBR功能可为优化编译器提供配置反馈数据。在考虑运行时开销时,与静态代码插装相比,LBR可能是更好的选择。
• 捕获函数参数。当LBR特性与PEBS一起使用时(参见第6.3节),可以捕获函数参数,因为根据x86调用约定,调用者的前几个参数落在寄存器中,这些寄存器被PEBS记录所捕获。【Int,2020年,附录B,第B.3.3.4章】
• 基本块执行计数。由于在LBR堆栈中分支IP(源)和之前的目标之间执行所有基本块恰好一次,因此可以评估程序内部基本块的执行率。这个过程涉及构建每个基本块起始地址的映射,然后向后遍历收集的LBR堆栈。【Int,2020年,附录B,第B.3.3.4章】
6.3 Processor Event-Based Sampling //处理器事件采样
处理器事件采样(PEBS)是CPU中的另一个非常有用的功能,提供了许多不同的方式来增强性能分析。与最后分支记录(参见第6.2节)类似,在对程序进行分析时使用PEBS来捕获每个收集样本的附加数据。在英特尔处理器中,PEBS功能是在NetBurst微架构中引入的。AMD处理器上类似的功能称为基于指令的采样(IBS),从Family 10h核心(代号为“ Barcelona”和“ Shanghai”)开始提供。
一组附加数据具有定义的格式,称为PEBS记录。当为PEBS配置性能计数器时,处理器将保存PEBS缓冲区的内容,随后存储到内存中。记录包含处理器的架构状态,例如通用寄存器(EAX,EBX,ESP等)的状态,指令指针寄存器(EIP),标志寄存器(EFLAGS)等。不同实现支持PEBS的PEBS记录布局在内容布局方面有所不同。有关枚举PEBS记录格式的详细信息,请参见[Int, 2020,卷3B,第18.6.2.4章处理器事件基础采样(PEBS)]。英特尔Skylake CPU的PEBS记录格式如图36所示。
Figure 36: PEBS Record Format for 6th Generation, 7th Generation and 8th Generation Intel
Core Processor Families. © Image from [Int, 2020, Volume 3B, Chapter 18].
用户可以通过执行dmesg命令来检查PEBS是否已启用:
- $ dmesg | grep PEBS
- [ 0.061116] Performance Events: PEBS fmt1+, IvyBridge events, 16-deep
- LBR, full-width counters, Intel PMU driver.
与LBR不同,Linux perf不能像LBR一样导出原始PEBS输出。相反,它处理PEBS记录并根据特定需求提取仅子集数据。因此,无法使用Linux perf访问原始PEBS记录的集合。虽然,Linux perf提供了从原始样本中处理的一些PEBS数据,可以通过perf report -D访问。要转储原始的PEBS记录,可以使用pebs-grabber工具。
PEBS机制在性能监测方面带来了许多好处,我们将在下一节中讨论。
6.3.1 Precise events //精确事件
在分析性能时,一个主要的问题是确定导致特定性能事件的确切指令。如第5.4节所述,基于中断的采样基于计数特定的性能事件,并等待直到它溢出。当发生溢出中断时,处理器需要一定的时间来停止执行并标记导致溢出的指令。这对于现代复杂的乱序CPU架构尤其困难。
它引入了滑动(skid)的概念,其定义为导致事件的IP与事件被标记的IP之间的距离(在PEBS记录内的IP字段中)。滑动使得难以发现实际导致性能问题的指令。考虑一个具有大量缓存未命中和热门汇编代码的应用程序:
- ; load1
- ; load2
- ; load3
分析器可能会将load3归因为导致大量缓存未命中的指令,而实际上,应该归咎于load1指令。这通常会给初学者带来很多困惑。有兴趣的读者可以在英特尔开发者社区网站上了解更多关于此类问题的根本原因。
通过让处理器本身在PEBS记录中存储指令指针(以及其他信息),可以缓解skid所带来的问题。PEBS记录中的EventingIP字段指示引起事件的指令。这需要硬件支持,并且通常仅适用于被称为“Precise Events”的一组支持的事件子集。特定微体系结构的精确事件列表可以在[Int, 2020, Volume 3B, Chapter 18]中找到。下面列出了Skylake微架构的精确事件:
- INST_RETIRED.*
- OTHER_ASSISTS.*
- BR_INST_RETIRED.*
- BR_MISP_RETIRED.*
- FRONTEND_RETIRED.*
- HLE_RETIRED.*
- RTM_RETIRED.*
- MEM_INST_RETIRED.*
- MEM_LOAD_RETIRED.*
- MEM_LOAD_L3_HIT_RETIRED.*
其中.*表示该组内的所有子事件都可以配置为精确事件。TMA方法(见第6.1节)在定位代码执行效率低下的源头时严重依赖于精确事件。通过使用精确事件减轻滑动的例子可以在easyperf博客中找到。Linux perf的用户应该给事件添加ppp后缀以启用精确标记:
- $ perf record -e cpu/event=0xd1,umask=0x20,name=MEM_LOAD_RETIRED.L3_MISS/ppp
- -- ./a.exe
6.3.2 Lower sampling overhead //降低采样开销
频繁生成中断并且分析工具本身捕获中断服务例程内的程序状态非常昂贵,因为它涉及操作系统交互。这就是为什么一些硬件允许自动多次采样到专用缓冲区而不需要任何中断。只有当专用缓冲区满时,处理器才会引发中断,并且将缓冲区刷新到内存中。这比传统的基于中断的采样具有更低的开销。
当性能计数器配置为PEBS时,计数器溢出条件将启动PEBS机制。在溢出后的下一个事件上,在处理器将生成一个PEBS事件。在PEBS事件上,处理器将PEBS记录存储在PEBS缓冲区域中,清除计数器溢出状态并重新加载计数器的初始值。如果缓冲区已满,则CPU会引发中断。[Int, 2020,Volume 3B, Chapter 18]请注意,PEBS缓冲区本身位于主内存中,其大小是可配置的。同样,性能分析工具的工作是为CPU分配和配置内存区域,以便能够将PEBS记录转储到其中。
6.3.3 Analyzing memory accesses
内存访问是许多应用程序性能的关键因素。通过PEBS,可以收集关于程序中内存访问的详细信息。实现这一功能的特性被称为数据地址分析(Data Address Profiling)。为了提供有关采样加载和存储的附加信息,它利用了PEBS设施内部的以下字段(参见图36):
- 数据线性地址(Data Linear Address,0x98)
- 数据源编码(Data Source Encoding,0xA0)
- 延迟值(Latency value,0xA8)
如果性能事件支持数据线性地址(DLA)机制,并且已启用,CPU将转储采样的内存访问的内存地址和延迟值。请记住,这个功能并不追踪所有的存储和加载操作,否则开销将会很大。相反,它在内存访问上进行采样,即仅分析每1000次访问中的一次。可以根据需要自定义每秒采样数的数量。
PEBS扩展中最重要的用例之一是检测真/假共享(True/False sharing)[145],我们将在第11.7节中讨论。Linux perf c2c工具在查找可能出现真/假共享的有争议内存访问时,大量依赖DLA数据。
此外,借助数据地址分析,用户可以获取程序中关于内存访问的一般统计信息:
- $ perf mem record -- ./a.exe
- $ perf mem -t load report --sort=mem --stdio
- # Samples: 656
- of event 'cpu/mem-loads,ldlat=30/P'
- # Total weight : 136578
- # Overhead Samples Memory access
- # ............................................
- 44.23% 267 LFB or LFB hit
- 18.87% 111 L3 or L3 hit
- 15.19% 78 Local RAM or RAM hit
- 13.38% 77 L2 or L2 hit
- 8.34% 123 L1 or L1 hit
根据输出结果,我们可以看到应用程序中有8%的加载操作命中了L1缓存,15%来自DRAM等等。
6.4 Intel Processor Traces
英特尔处理器追踪(Intel Processor Traces,简称PT)是一项CPU功能,通过以高度压缩的二进制格式编码数据包来记录程序执行情况,并在每条指令上附加时间戳,可以用于重构执行流程。PT具有广泛的覆盖范围和相对较小的开销[146],通常低于5%。它的主要用途是进行事后分析和定位性能故障的根本原因。
6.4.1 Workflow
与采样技术类似,PT不需要对源代码进行任何修改。您只需在支持PT的工具下运行程序以收集追踪数据。一旦启用了PT并启动了基准测试,分析工具就会开始将跟踪数据包写入DRAM。
Figure 37: Intel Processor Traces encoding
与LBR类似,Intel PT通过记录分支来工作。在运行时,每当CPU遇到分支指令时,PT将记录该分支的结果。对于简单的条件跳转指令,CPU将使用1位记录是否跳转(T)或不跳转(NT)。对于间接调用,PT将记录目标地址。需要注意的是,我们会忽略无条件分支,因为我们在静态分析中已经知道它们的目标地址。
以下是一个小指令序列的编码示例,如图37所示。像PUSH、MOV、ADD和CMP这样的指令被忽略,因为它们不改变控制流程。然而,JE指令可能会跳转到.label,所以需要记录其结果。之后有一个间接调用,需要保存目标地址。
Figure 38: Intel Processor Traces decoding
在分析时,我们将应用程序二进制文件和收集的PT追踪数据结合起来。软件解码器需要应用程序二进制文件以重构程序的执行流程。它从入口点开始,然后使用收集的追踪数据作为查找参考来确定控制流程。图38展示了Intel Processor Traces的解码示例。
假设PUSH指令是应用程序二进制文件的入口点。然后,PUSH、MOV、ADD和CMP指令将按原样重构,而无需查看编码的追踪数据。然后,软件解码器遇到了JE指令,它是一个条件分支,我们需要查找其结果。根据图38中的追踪数据,JE被执行(T),因此我们跳过下一个MOV指令并转到CALL指令。同样,CALL(edx)是一个改变控制流程的指令,因此我们在编码的追踪数据中查找目标地址,即0x407e1d8。在我们的程序运行时,以黄色高亮显示的指令被执行。请注意,这是对程序执行的精确重构;我们没有跳过任何指令。随后,我们可以使用调试信息将汇编指令映射回源代码,并记录一行接一行执行的源代码日志。
6.4.2 Timing Packets //时序数据包
使用Intel PT,不仅可以追踪执行流程,还可以获得时序信息。除了保存跳转目标外,PT还可以发出时序数据包。图39展示了如何使用时序数据包来恢复指令的时间戳。与前面的示例类似,首先我们可以看到JNZ没有被执行,因此我们将它和上面的所有指令的时间戳更新为0纳秒。然后我们看到一个2纳秒的时间更新和JE被执行,所以我们将它和JE上面(以及JNZ下面)的所有指令的时间戳更新为2纳秒。之后,有一个间接调用,但没有附带时序数据包,所以我们不会更新时间戳。然后我们看到100纳秒经过了,JB没有被执行,所以我们将它上面的所有指令的时间戳更新为102纳秒。
Figure 39: Intel Processor Traces timings
在图39中显示的示例中,指令数据(控制流)是完全准确的,但时序信息不太准确。显然,CALL(edx)、TEST和JB指令并不是同时发生的,然而我们没有更准确的时序信息来描述它们。有时间戳可以使我们将程序的时间间隔与系统中的其他事件对齐,并且可以轻松与挂钟时间进行比较。在某些实现中,跟踪的时序信息可以通过精确的周期模式进一步提高,其中硬件在正常数据包之间记录周期计数(更多详细信息参见[Int,2020,Volume 3C,Chapter 36])。
6.4.3 Collecting and Decoding Traces
可以使用Linux perf工具轻松收集Intel PT跟踪数据:
$ perf record -e intel_pt/cyc=1/u ./a.out
在上述命令行中,我们要求PT机制每个时钟周期更新时序信息。但是,由于时序数据包只会在与其他控制流数据包配对时发送,因此这可能不会极大地提高准确性(参见第6.4.2节)。
收集完成后,可以通过执行以下命令获取原始的PT跟踪数据:
$ perf report -D > trace.dump
PT在发出时序数据包之前会捆绑多达6个条件分支。自从Intel Skylake CPU一代以来,时序数据包记录的是与上一个数据包相对的周期计数。如果我们查看trace.dump文件,可能会看到类似以下的内容:
- 000073b3: 2d 98 8c TIP 0x8c98 // target address (IP)
- 000073b6: 13 CYC 0x2 // timing update
- 000073b7: c0 TNT TNNNNN (6) // 6 conditional branches
- 000073b8: 43 CYC 0x8 // 8 cycles passed
- 000073b9: b6 TNT NTTNTT (6)
在上面,我们展示了原始的PT数据包,这对于性能分析来说并不是非常有用。要将处理器跟踪解码为可读形式,可以执行以下操作:
$ perf script --ns --itrace=i1t -F time,srcline,insn,srccode
下面是可能得到的解码跟踪的示例:
- timestamp srcline instruction srccode
- ...
- 253.555413143: a.cpp:24 call 0x35c foo(arr, j);
- 253.555413143: b.cpp:7 test esi, esi for (int i = 0; i <= n; i++)
- 253.555413508: b.cpp:7 js 0x1e
- 253.555413508: b.cpp:7 movsxd rsi, esi
- ...
上面只显示了执行日志的一小部分。在这个日志中,我们记录了程序运行时执行的每条指令的跟踪信息。我们可以逐步观察程序的每一步操作。这为进一步的分析提供了非常坚实的基础。
6.4.4 Usages
以下是 PT 在以下情况下可能有用的几个案例:
1. 分析性能故障。由于 PT 捕获了整个指令流,因此可以分析在应用程序无响应的短时间内发生了什么。在 easyperf 博客的一篇文章中可以找到更详细的示例。
2. 后期调试。PT 跟踪数据可以由传统的调试器(如 gdb)进行回放。除此之外,PT 还提供了调用栈信息,即使堆栈已损坏,这些信息也始终有效。可以在远程机器上收集 PT 跟踪数据,然后进行离线分析。这在问题难以复现或系统访问受限时特别有用。
3. 检查程序的执行情况。
- 我们可以立即知道某些代码路径是否未执行。
- 借助时间戳,可以计算在尝试获取锁时等待的时间等。
- 通过检测特定的指令模式来进行安全缓解。
6.4.5 Disk Space and Decoding Time
即使考虑到跟踪数据的压缩格式,编码数据可能会占用大量的磁盘空间。通常情况下,每条指令的编码数据少于1字节,但考虑到CPU执行指令的速度,仍然非常大。根据工作负载的不同,CPU对PT进行编码的速度通常为100MB/s。解码后的跟踪数据可能会是编码数据的十倍(约1GB/s)。这使得在长时间运行的工作负载上使用PT变得不切实际。但是,在小型工作负载上运行一小段时间是可以接受的,即使工作负载很大。在这种情况下,用户可以只在故障发生的时间段内附加到运行中的进程。或者他们可以使用循环缓冲区,其中新的跟踪数据将覆盖旧的数据,即始终保留最近大约10秒的跟踪数据。
用户可以通过多种方式进一步限制收集。他们可以只限制收集用户/内核空间代码的跟踪数据。此外,还有一个地址范围过滤器,可以动态选择性地开启和关闭跟踪以限制内存带宽。这使我们可以跟踪单个函数甚至单个循环。
解码 PT 跟踪数据可能需要很长时间。在一个搭载 Intel Core i5-8259U 的机器上,对于运行时间为7毫秒的工作负载,编码后的 PT 跟踪数据约占用1MB的磁盘空间。使用 perf script 解码这条跟踪数据大约需要20秒。perf script -F time,ip,sym,symoff,insn 的解码输出占用约1.3GB的磁盘空间。
个人经验:Intel PT被认为是性能分析的终极解决方案。由于其低运行时开销,它是一个非常强大的分析功能。然而,目前(2020年2月),使用'perf script -F'命令和'+srcline'或'+srccode'选项来解码跟踪数据的速度非常慢,不适合日常使用。Linux perf的实现可能需要改进。Intel VTune Profiler对于PT的支持仍然是实验性的。
参考资料和链接:
• Intel出版物《处理器跟踪》:[链接](https://software.intel.com/en-us/blogs/2013/09/18/processor-tracing)
• Intel® 64和IA-32体系结构软件开发人员手册[Int, 2020,卷3C,第36章]
• 白皮书《硬件辅助的指令分析和延迟检测》[Sharma和Dagenais,2016]
• Andi Kleen在LWN上的文章:[链接](https://lwn.net/Articles/648154)
• Intel PT微教程:[链接](https://sites.google.com/site/intelptmicrotutorial/)
• simple_pt:用于Linux的简单Intel CPU处理器跟踪工具:[链接](https://github.com/andikleen/simple-pt/)
• Linux内核中的Intel PT文档:[链接](https://github.com/torvalds/linux/blob/master/tools/perf/Documentation/intel-pt.txt)
• Intel处理器跟踪速查表:[链接](http://halobates.de/blog/p/410)
这些参考资料和链接提供了关于Intel PT的更多信息和工具,以及与之相关的开发和调试技术。希望对您有所帮助!如果您有更多问题,请随时提问。
6.5 Chapter Summary
• 推荐在解决了所有高级性能问题之后,才利用硬件功能进行低级调优。对于设计不良的算法进行调优是一种错误的投资开发者时间的做法。一旦消除了所有主要性能问题,就可以使用CPU性能监测功能来分析和进一步调优应用程序。
• Top-Down微体系结构分析(TMA)方法是一种非常强大的技术,用于识别程序对CPU微体系结构的无效使用。这是一种健壮且正式的方法,即使对于经验不足的开发人员来说也很容易使用。TMA是一个迭代的过程,包括多个步骤,包括对工作负载进行表征,并定位瓶颈出现的确切位置在源代码中。我们建议TMA应该是每个低级调优努力的分析起点。TMA在Intel和AMD处理器上都可用。
• 最后分支记录(LBR)机制在执行程序的同时连续记录最近的分支结果,造成最小的减速。它使我们能够为我们收集的每个分析样本具有足够深的调用堆栈。此外,LBR还有助于识别热分支、错误预测率,并允许对机器码进行精确计时。LBR在Intel和AMD处理器上都受支持。
• 处理器事件采样(PEBS)功能是另一种用于分析的增强功能。它通过自动在专用缓冲区中多次进行采样来降低采样开销,无需中断。然而,PEBS更广为人知的是引入了“精确事件”,可以精确定位导致特定性能事件的确切指令。该功能在Intel处理器上受支持。AMD CPU具有类似功能称为基于指令采样(IBS)。
• Intel处理器跟踪(PT)是一种CPU功能,通过以高度压缩的二进制格式记录程序执行,可以用于在每条指令上重构执行流程,并带有时间戳。PT具有广泛的覆盖范围和相对较小的开销。其主要用途是事后分析和查找性能故障的根本原因。基于ARM架构的处理器也具有称为CoreSight的跟踪能力,但主要用于调试而不是性能分析。
性能分析工具利用本章介绍的硬件功能实现了许多不同类型的分析。
//目前看,能借鉴的是TMA和LBR!!!
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。