5 Performance Analysis Approaches

进行高级优化时,通常很容易判断性能是否得到改善。当您编写算法的更好版本时,您希望看到程序运行时间的明显差异。但是,有时候您会看到执行时间的变化,但是您不知道它来自哪里。单独的时间无法提供任何关于发生这种情况的原因的见解。在这种情况下,我们需要更多关于程序执行方式的信息。这是我们需要进行性能分析以了解所观察到的减速或加速的底层本质的情况。
我们的程序运行时,硬件和软件都会跟踪性能数据。在这种情况下,所谓的硬件是指执行程序的CPU,而所谓的软件是指操作系统和所有用于分析的工具。通常,软件堆栈提供高级度量标准,如时间、上下文切换次数和页面错误,而CPU能够观察缓存未命中、分支预测错误等。根据我们试图解决的问题,一些指标比其他指标更有用。因此,硬件指标并不意味着总是能给我们一个更精确的程序执行概述。例如,某些指标(例如上下文切换次数)CPU无法提供。性能分析工具(例如Linux perf)可以从操作系统和CPU中消费数据。
我们将在本书中广泛使用Linux perf,因为它是最受欢迎的性能分析工具之一。该工具适用于大多数Linux发行版,因此对于广泛的用户来说非常方便。作者喜欢展示Linux perf的另一个原因是它是开源的,这使我们能够看到典型分析工具中正在发生的机制。这对于学习本书中提出的概念尤其有用,因为基于GUI的工具(例如Intel® VTune™ Profiler)往往会隐藏所有复杂性。有关Linux perf的更多信息,可参考其Wiki页面。
在本章中,我们将介绍一些最受欢迎的性能分析技术:代码插装、跟踪、特征化和采样。我们还将讨论不涉及运行实际应用程序的静态性能分析技术和编译器优化报告。
simpleperf如何查看线程上下文切换次数?
在使用simpleperf工具时,要查看线程上下文切换次数,可以使用`report`命令结合`--show-sched-events`选项来生成报告。具体操作如下:
1. 使用`perf record`命令记录程序的性能数据,例如:

```
$ simpleperf record -p <pid> -e context-switches
```

2. 使用`perf report`命令结合`--show-sched-events`选项来生成报告,例如:

```
$ simpleperf report --show-sched-events
```

运行以上命令后,会显示出有关程序的性能数据报告,其中包括线程上下文切换次数等信息。
值得注意的是,需要在运行简单性能工具(simpleperf)时使用root权限或者配置kernel.perf_event_paranoid参数的值为-1才能记录一些事件,例如线程上下文切换次数。
simpleperf如何查看多线程同步时间?
在使用simpleperf工具时,要查看多线程同步时间,可以使用`report`命令结合`--show-mutex-contention`选项来生成报告。具体操作如下:
1. 使用`perf record`命令记录程序的性能数据,例如:

```
$ simpleperf record -p <pid> -e cycles -e sched:sched_switch -e sched:sched_stat_sleep
```

上面的命令中,我们同时记录了程序的CPU周期数、进程切换以及进程睡眠事件。
2. 使用`perf report`命令结合`--show-mutex-contention`选项来生成报告,例如:

```
$ simpleperf report --show-mutex-contention
```

运行以上命令后,会显示出有关程序的性能数据报告,其中包括多线程同步时间等信息。`--show-mutex-contention`选项会显示互斥量竞争的相关信息,其中包括持有者、等待者、竞争次数和等待时间等信息。
注意:需要在编译程序时加入 `-phtreads` 选项,才能记录对多线程互斥锁的竞争。
//contention 竞争
5.1 Code Instrumentation    //代码插桩
Code Instrumentation的中文含义是“代码插装”,它是在程序代码中插入额外的代码以收集和分析执行时间和性能数据的过程。Code Instrumentation能够帮助开发人员识别性能瓶颈、调试代码错误、分析代码行为等。常见的Code Instrumentation技术包括手动插装、运行时插装和静态插装等。手动插装是通过在代码中手动添加计时器或日志语句来收集数据的方法,但是这种方法会增加代码复杂性,并且难以扩展到大型代码库中。运行时插装是通过在程序运行时动态地插入代码来收集数据的方法,需要使用特殊的编译器或工具支持。静态插装是在编译时将插装代码注入到源代码中,可以自动化生成代码并保持代码基础结构的完整性,但是需要一些高级编译器的支持。
Code Instrumentation可能是最早发明的性能分析方法之一,它是一种通过向程序中插入额外的代码来收集运行时信息的技术。如代码清单6所示,最简单的插装方式就是在函数开头插入printf语句来计算函数调用次数。我相信世界上的每个程序员都至少做过一次这样的事情。当你需要了解程序执行的特定情况时,这种方法提供了非常详细的信息。Code Instrumentation使我们能够追踪程序中每个变量的任何信息。
Listing 6 Code Instrumentation

int foo(int x) {
printf("foo is called");
// function body...
}

基于插装的性能分析方法主要用于宏观层面,而不是微观层面。使用这种方法通常可以在优化大型代码块时提供最好的洞察力,因为你可以采用自上而下的方法(从插装主函数开始,然后深入到它的被调用函数)来定位性能问题。虽然对于小型程序,代码插装并不是非常有帮助,但它通过让开发人员观察应用程序的架构和流程来提供最有价值和深入的见解。这种技术对于正在处理陌生代码库的人尤其有帮助。
值得一提的是,在具有许多不同组件的复杂系统中,代码插装显示出了其优越性,因为这些组件根据输入或时间的不同反应也会不同。采样技术(在第5.4节中讨论)压缩了有价值的信息,无法帮助我们检测异常行为。例如,在游戏中,通常有渲染线程、物理线程、动画线程等。插装这样的大型模块可以帮助我们相对快速地了解问题的源头。有时候,优化不仅仅是优化代码,还涉及到数据方面的问题。例如,渲染过慢是因为网格未压缩,或者场景中对象太多导致物理计算过慢等等。这种技术在实时场景中广泛应用,例如视频游戏和嵌入式开发。许多性能分析工具将插装与本章讨论的其他技术(跟踪、采样)混合使用。
虽然代码插装在许多情况下非常有用,但它无法提供关于代码从操作系统或CPU角度的执行方式的任何信息。例如,它无法告诉你进程被调度执行的频率(由操作系统获知)或分支预测出现了多少错误(由CPU获知)。插装代码是应用程序的一部分,并具有与应用程序本身相同的特权。它在用户空间运行,无法访问内核。
但更重要的是,这种技术的缺点是,每次需要插装新内容(例如另一个变量)时,都需要重新编译。这可能会成为工程师的负担,并增加分析时间。不幸的是,这并不是全部的缺点。因为通常情况下,您关心的是应用程序中的热点路径,所以您会插装驻留在代码性能关键部分的内容。将插装代码插入热点代码中可能会轻松导致整个基准测试速度减慢2倍。最后,通过对代码进行插装,您将改变程序的行为,因此您可能看不到先前看到的效果。
所有这些都增加了实验之间的时间,并消耗了更多的开发时间,这就是为什么工程师现在很少手动插装他们的代码。然而,自动代码插装仍然被编译器广泛使用。编译器能够自动插装整个程序,并收集关于执行的有趣统计信息。最广为人知的用例是代码覆盖率分析和基于分析的优化(见第 7.7 节)。
在谈论插装时,提及二进制插装技术非常重要。二进制插装的思想类似,但是是针对已构建的可执行文件而不是源代码级别进行的。有两种类型的二进制插装:静态(预先完成)和动态(插装代码在程序执行时根据需要插入)。动态二进制插装的主要优点是它不需要程序重新编译和重新链接。此外,通过动态插装,可以将插装的数量限制在仅有趣的代码区域中,而不是整个程序。
二进制插装在性能分析和调试中非常有用。最流行的二进制插装工具之一是 Intel Pin 工具。Pin 拦截程序在发生有趣事件时的执行,并生成从程序中该点开始的新插装代码。它允许收集各种运行时信息,例如:
• 指令计数和函数调用计数。
• 拦截应用程序中的函数调用和任何指令的执行。
• 允许通过在区域开始处捕获内存和硬件寄存器状态来“记录和回放”程序区域。
与代码插装一样,二进制插装仅允许插装用户级代码,并且可能非常慢。
5.2 Tracing    //跟踪
跟插装相比,追踪的概念非常相似但有所不同。代码插装假定用户可以编排应用程序的代码。另一方面,追踪依赖于程序外部依赖项的现有插装。例如,strace 工具允许我们跟踪系统调用,并可以视为 Linux 内核的插装。Intel 处理器跟踪(见第 6.4 节)允许记录程序执行的指令,并可以视为 CPU 的插装。追踪可以从事先适当插装的组件中获取,并且不受更改的影响。追踪通常用作黑箱方法,其中用户无法修改应用程序的代码,但他们想了解程序在幕后正在做什么。
使用 Linux strace 工具跟踪系统调用的示例在清单 7 中演示。该清单显示运行 git status 命令时输出的前几行。通过使用 strace 追踪系统调用,可以知道每个系统调用的时间戳(最左侧的列),它的退出状态以及每个系统调用的持续时间(在尖括号中)。
Listing 7 Tracing system calls with strace.

$ strace -tt -T -- git status
17:46:16.798861 execve("/usr/bin/git", ["git", "status"], 0x7ffe705dcd78 /* 75 vars */) = 0 <0.000300>
17:46:16.799493 brk(NULL) = 0x55f81d929000 <0.000062>
17:46:16.799692 access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory) <0.000063>
17:46:16.799863 access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory) <0.000074>
17:46:16.800032 openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3 <0.000072>
17:46:16.800255 fstat(3, {st_mode=S_IFREG|0644, st_size=144852, ...}) = 0 <0.000058>
17:46:16.800408 mmap(NULL, 144852, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f6ea7e48000 <0.000066>
17:46:16.800619 close(3) = 0 <0.000123>
...

追踪的开销非常取决于我们尝试追踪的内容。例如,如果我们追踪几乎从不进行系统调用的程序,则在 strace 下运行它的开销将接近零。相反,如果我们追踪严重依赖系统调用的程序,开销可能非常大,如 100x。此外,追踪可能会生成大量数据,因为它不会跳过任何采样。为了弥补这一点,追踪工具提供了仅为特定时间片或代码片段收集数据的手段。
通常,与插装类似,追踪被用于探索系统中的异常情况。例如,您可能想查找应用程序在不响应的 10 秒钟期间发生了什么。性能分析不是为此设计的,但是通过追踪,可以看到导致程序不响应的原因。例如,使用 Intel PT(见第 6.4 节),我们可以重构程序的控制流,并确切地知道执行了哪些指令。
追踪还非常有用于调试。其底层特性使得基于记录的追踪进行“记录和重放”用例成为可能。其中一种工具是 Mozilla rr74 调试器,该调试器记录和重放进程,允许后退单步执行等。大多数追踪工具都能够使用时间戳装饰事件(请参见清单 7 中的示例),这样可以使我们与在那段时间发生的外部事件进行关联。也就是说,当我们观察到程序中出现故障时,我们可以查看应用程序的追踪,并将此故障与整个系统在那段时间内发生的情况进行关联。
5.3 Workload Characterization    //工作负载特征分析
工作负载特性分析是通过定量参数和函数描述工作负载的过程。其目的是定义工作负载的行为及其最重要的特征。从高层次上看,应用程序可以属于以下一种或多种类型:交互式、数据库、基于网络、并行等。针对不同的应用领域,可以使用不同的度量和参数来表征不同的工作负载。
在第6.1节中,我们将仔细研究自上而下微架构分析(TMA)方法,该方法试图通过将应用程序分为以下4个部分之一来描述应用程序:前端受限、后端受限、退役和恶意投机。TMA使用性能监控计数器(PMC,请参见第3.9.1节)来收集所需信息并确定CPU微架构的低效使用。
5.3.1 Counting Performance Events    //计数性能事件
PMCs是低级别性能分析的非常重要工具。它们可以提供关于程序执行的独特信息。PMCs通常使用两种模式:“计数”和“采样”。计数模式用于工作负载特性分析,而采样模式用于查找热点,在第5.4节中我们将讨论这一点。计数的思想非常简单:我们想在程序运行期间统计某些性能事件的数量。图18说明了在时间维度上计数性能事件的过程。

Figure 18: Counting performance events.
图18中概述的步骤大致代表了典型分析工具用于计数性能事件的过程。这个过程在perf stat工具中实现,该工具可以用于计数各种硬件事件,如指令数、周期数、缓存失效等。下面是perf stat输出的示例:

$ perf stat -- ./a.exe
10580290629 cycles    # 3,677 GHz
8067576938 instructions    # 0,76 insn per cycle
3005772086 branches    # 1044,472 M/sec
239298395 branch-misses    # 7,96% of all branches

了解这些数据非常有益。首先,它允许我们快速发现一些异常,比如高缓存失效率或低IPC。其次,当您刚刚进行了代码改进并且想要验证性能增益时,它可能会很有用。查看绝对数字可以帮助您证明或拒绝代码更改。
个人经验:我将“perf stat”用作简单的基准包装器。由于计数事件的开销最小,我几乎自动在“perf stat”下运行所有基准。它为我提供了性能调查的第一步。有时可以立即发现异常,这可以节省一些分析时间。
5.3.2 Manual performance counters collection    //手动收集性能计数
现代CPU有数百个可计数的性能事件。记住它们及其含义非常困难。理解何时使用特定的PMC甚至更难。这就是为什么通常我们不建议手动收集特定的PMC,除非您确实知道自己在做什么。相反,我们建议使用像Intel Vtune Profiler这样的工具来自动化这个过程。然而,在某些情况下,您可能有兴趣收集特定的PMC。
所有Intel CPU世代的性能事件的完整列表可以在[Int,2020,Volume 3B,Chapter 19]中找到。每个事件都用事件和Umask十六进制值进行编码。有时性能事件还可以使用其他参数进行编码,例如Cmask和Inv等。下表4显示了对Intel Skylake微架构编码两个性能事件的示例。

Table 4: Example of encoding Skylake performance events.
Linux perf为常用的性能计数器提供了映射。它们可以通过伪名称访问,而不是指定事件和Umask十六进制值。例如,branches只是BR_INST_RETIRED.ALL_BRANCHES的同义词,将测量相同的事物。perf list可以查看可用映射名称的列表。

$ perf list
branches [Hardware event]
branch-misses [Hardware event]
bus-cycles [Hardware event]
cache-misses [Hardware event]
cycles [Hardware event]
instructions [Hardware event]
ref-cycles [Hardware event]

然而,Linux perf并没有为每个CPU架构的所有性能计数器提供映射。如果您要查找的PMC没有映射,可以使用以下语法进行收集:

$ perf stat -e cpu/event=0xc4,umask=0x0,name=BR_INST_RETIRED.ALL_BRANCHES/ -- ./a.exe

此外,还有一些Linux perf的包装程序可以进行映射,例如oprofile和ocperf.py。以下是它们的使用示例:

$ ocperf -e uops_retired ./a.exe
$ ocperf.py stat -e uops_retired.retire_slots -- ./a.exe

性能计数器并不是在每个环境中都可以使用的,因为访问PMCs需要root权限,而运行在虚拟化环境中的应用程序通常不具备root权限。对于在公共云中执行的程序,如果虚拟机管理器没有正确地向客户端公开PMU编程接口,则直接在客户端容器中运行基于PMU的分析器将无法生成有用的输出。因此,基于CPU性能计数器的分析器在虚拟化和云环境中的效果并不好[Du et al., 2010]。尽管情况正在改善。VmWare®是最早支持虚拟CPU性能计数器(vPMC)的虚拟机管理器之一。AWS EC2云为专用主机启用了PMC。
5.3.3 Multiplexing and scaling events    //多路复用和事件扩展
有时我们需要同时计算许多不同的事件。但是只有一个计数器,每次只能计算一个事情。这就是为什么PMU内置了多个计数器(通常每个硬件线程有4个)。即使这样,固定和可编程计数器的数量也不总是足够的。Top-Down分析方法需要在单个程序执行中收集多达100个不同的性能事件。显然,CPU没有那么多计数器,这就需要使用多路复用。
如果事件比计数器多,分析工具使用时间多路复用,让每个事件有机会访问监视硬件。图19显示了在只有4个PMCs可用的情况下,8个性能事件之间的多路复用示例。

Figure 19: Multiplexing between 8 performance events with only 4 PMCs available.
通过多路复用,事件并非始终被测量,而只在其中的某个时段内进行测量。在运行结束时,性能分析工具需要根据总启用时间对原始计数进行缩放:
最终计数 = 原始计数 * (运行时间 / 启用时间)
例如,在进行性能分析时,我们可以在五个时间间隔内测量某个计数器。每个测量间隔为100ms(启用时间)。程序运行时间为1秒(运行时间)。此计数器的总事件数测量为10000(原始计数)。因此,我们需要将最终计数缩放2倍,即20000:
最终计数 = 10000 * (1000ms/500ms) = 20000
这提供了一个估计值,如果该事件在整个运行期间内被测量,它将是多少。非常重要的是要理解,这只是一个估计值,而不是一个实际计数。多路复用和缩放可以安全地应用于执行长时间间隔内相同代码的稳定工作负载上。相反,如果程序经常在不同的热点之间跳转,就会出现盲区,在缩放过程中可能会产生错误。为避免缩放,可以尝试将要测量的事件数量不超过可用物理 PMCs 的数量。然而,这将需要多次运行基准测试来测量所感兴趣的所有计数器。
5.4 Sampling    //采样
采样是执行性能分析时最常用的方法。人们通常将其与在程序中找到热点相关联。一般来说,采样给出了一个问题的答案:哪个代码位置对某些性能事件做出了最大的贡献。如果我们考虑寻找热点,这个问题可以重新定义为:哪个代码位置消耗了最多的 CPU 周期。人们通常使用“分析”一词来代替技术上称为“采样”的术语。根据维基百科的定义,分析是一个更广泛的术语,包括收集数据的各种技术,例如中断、代码插装和 PMC 等。
可能会让人惊讶的是,人们可以通过调试器实现最简单的采样分析工具。事实上,您可以通过以下步骤识别热点:a)在调试器下运行程序,b)每隔10秒暂停程序,c)记录它停止的地方。如果您重复执行 b)和 c)很多次,您将构建一个样本集合。您停止的代码行将是程序中最热的位置。当然,这只是实际分析工具如何工作的简单描述。现代分析工具能够每秒收集数千个样本,这可为基准测试中最热门的位置提供相当精确的估计。与调试器的示例类似,每当捕获一个新样本时,分析工具都会中断要分析的程序的执行。在中断时,分析工具会收集程序状态的快照,构成一个样本。收集的信息可能包括中断时执行的指令地址、寄存器状态、调用堆栈(参见第5.4.3节)等。收集的样本数据存储在数据采集文件中,可以进一步用于显示调用图、程序中最耗时部分和统计重要代码部分的控件流。
5.4.1 User-Mode And Hardware Event-based Sampling    //用户模式和基于硬件事件的采样
采样可以以两种不同的模式进行,即使用用户模式或基于硬件事件的采样(EBS)。用户模式采样是一种纯软件方法,它将代理库嵌入到被检测应用程序中。代理为应用程序中的每个线程设置操作系统计时器。当计时器到期时,应用程序接收处理采集器处理的 SIGPROF 信号。EBS 使用硬件性能监视计数器(PMCs)来触发中断。特别地,使用 PMU 的计数器溢出特性,我们将在下一节中讨论。用户模式采样只能用于识别热点,而 EBS 则可用于涉及 PMCs 的其他分析类型,例如基于缓存未命中的采样、TMA(请参见第6.1节)等。
用户模式采样比 EBS 产生更多的运行时开销。当使用默认间隔10ms进行采样时,用户模式采样的平均开销约为5%。在1ms采样间隔下,基于事件的采样的平均开销约为2%。通常,EBS 更准确,因为它允许以更高的频率收集样本。但是,用户模式采样生成的数据要少得多,分析数据所需的时间也更短。
5.4.2 Finding Hotspots
在本节中,我们将讨论使用 PMCs 与 EBS 的情况。图20说明了采用 PMU 的计数器溢出特性来触发性能监视中断(PMI)。

Figure 20: Using performance counter for sampling
首先,我们需要配置要进行采样的事件。识别热点意味着知道程序花费大部分时间的地方。因此,基于循环采样是非常自然的选择,并且它是许多性能分析工具的默认配置。但这并不一定是严格的规则;我们可以对任何想要的性能事件进行采样。例如,如果我们想知道程序遇到最多L3缓存未命中的位置,我们将在相应事件上进行采样,即MEM_LOAD_RETIRED.L3_MISS。
准备工作完成后,我们启用计数并让基准测试运行。我们将PMC配置为计算周期数,因此每个周期都会增加计数器的值。最终,计数器将溢出。计数器溢出时,硬件将引发PMI。性能分析工具被配置为捕获PMI,并具有用于处理它们的中断服务例程(ISR)。在这个例程内部,我们需要执行多个步骤:首先,我们需要禁用计数;然后,记录在计数器溢出时CPU执行的指令;接着,将计数器重置为N并恢复基准测试。
现在,回到值N。使用这个值,我们可以控制想要多频繁获得新的中断。假设我们希望有更细的粒度,并且每1百万条指令采集一次样本。为了实现这一点,我们可以将计数器设置为-1百万,这样它将在每1百万条指令后溢出。这个值通常被称为“采样后”的值。
我们重复这个过程很多次,以建立足够的示例集合。如果以后聚合这些样本,我们可以构建程序中最热门地方的直方图,如下面从Linux perf record/report的输出所示。这给出了程序函数开销的按降序排序的分解(热点)。
以下是对来自Phoronix测试套件的x264基准测试进行采样的示例:

$ perf record -- ./x264 -o /dev/null --slow --threads 8
Bosphorus_1920x1080_120fps_420_8bit_YUV.y4m
$ perf report -n --stdio
# Samples: 364K of event 'cycles:ppp'
# Event count (approx.): 300110884245
# Overhead Samples Shared Object Symbol
# ....................................................................
#
6.99% 25349 x264 [.] x264_8_me_search_ref
6.70% 24294 x264 [.] get_ref_avx2
6.50% 23397 x264 [.] refine_subpel
5.20% 18590 x264 [.] x264_8_pixel_satd_8x8_internal_avx2
4.69% 17272 x264 [.] x264_8_pixel_avg2_w16_sse2
4.22% 15081 x264 [.] x264_8_pixel_avg2_w8_mmx2
3.63% 13024 x264 [.] x264_8_mc_chroma_avx2
3.21% 11827 x264 [.] x264_8_pixel_satd_16x8_internal_avx2
2.25% 8192 x264 [.] rd_cost_mb
...

那么,我们自然希望知道热点列表中出现的每个函数内部的热点代码。要查看已内联的函数的分析数据以及特定源代码区域生成的汇编代码,需要使用带有调试信息(-g编译器标志)构建应用程序。用户可以使用-gline-tables-only选项将调试信息减少为符号在源代码中出现的行号。像Linux perf这样没有完整、丰富图形支持的工具通常会将源代码与生成的汇编混合在一起,如下所示:

# snippet of annotating source code of 'x264_8_me_search_ref' function
$ perf annotate x264_8_me_search_ref --stdio
Percent | Source code & Disassembly of x264 for cycles:ppp
----------------------------------------------------------
...
: bmx += square1[bcost&15][0]; <== source code
1.43 : 4eb10d: movsx ecx,BYTE PTR [r8+rdx*2] <== corresponding
machine code
: bmy += square1[bcost&15][1];
0.36 : 4eb112: movsx r12d,BYTE PTR [r8+rdx*2+0x1]
: bmx += square1[bcost&15][0];
0.63 : 4eb118: add DWORD PTR [rsp+0x38],ecx
: bmy += square1[bcost&15][1];
...

大多数带有图形用户界面(GUI)的性能分析工具,例如Intel VTune Profiler,可以将源代码和相关汇编代码并排显示,如图21所示。

Figure 21: Intel® VTune™ Profifiler source code and assembly view for x264 benchmark.
iphone上可以运行simpleperf么?
简短的回答是不行。simpleperf是一个基于Linux内核性能计数器的工具,因此它不能在iPhone上运行。另外,iPhone OS是一个专有的操作系统,它具有与Linux内核不同的底层架构和API,因此simpleperf在其上的运行是不可能的。
iphone上类似simpleperf的工具有什么?
对于iPhone设备,有一些类似simpleperf的性能分析工具可供选择。以下是其中的一些工具:
1. Xcode Instruments:这是苹果官方提供的工具,它可以帮助开发者对CPU、内存、网络等方面进行分析,同时提供了各种详细的图表和数据报告。
2. Shark:Shark是苹果早期提供的一个性能分析工具,现在已经不再更新,但仍然可以使用。它提供了丰富的分析选项和图表,并支持多种文件输出格式。
3. Instruments-lite:这是一个第三方开发的工具,它基于Xcode Instruments的API,提供了与简单的界面和一些额外的功能,例如查看进程信息、日志输出等。
4. Sysdiagnose:这也是一个由苹果官方提供的工具,它主要用于收集系统和应用程序的日志信息以进行分析。可以通过在设备上输入特定的命令来启动Sysdiagnose。
请注意,这些工具可能在不同版本的iOS系统上有所差异,因此建议仔细查看每个工具的文档以了解其功能和使用方式。
5.4.3 Collecting Call Stacks
在进行采样时,我们经常会遇到这样一种情况:程序中最热门的函数被多个调用者调用。例如,图22展示了这种情况的一个示例。分析工具的输出可能会显示,函数foo是程序中最热门的函数之一,但如果它有多个调用者,我们希望知道哪一个调用者调用foo的次数最多。对于像memcpy或sqrt这样出现在热点函数中的库函数,这是一个典型的情况。为了理解为什么某个特定的函数出现在热点中,我们需要知道程序的控制流图(CFG)中哪条路径引起了它的出现。

Figure 22: Control Flow Graph: hot function “foo” has multiple callers.
分析所有调用foo的调用者的逻辑可能非常耗时。我们只想关注那些导致foo成为热点的调用者。换句话说,我们想知道程序控制流图中最热门的路径。性能分析工具通过在收集性能样本时捕获进程的调用堆栈以及其他信息来实现这一点。然后,所有收集到的堆栈会被分组,使我们能够看到导致特定函数的热门路径。
在Linux perf中收集调用堆栈有三种方法:
1. Frame Pointers (FP):使用Frame Pointers方法收集调用堆栈需要在编译时为代码添加--fno-omit-frame-pointer选项来禁用省略框架指针。历史上,框架指针(RBP)用于调试,因为它允许我们获取调用堆栈而不必从堆栈中弹出所有参数(堆栈展开)。框架指针可以直接告诉返回地址。然而,它消耗了一个寄存器来执行这个任务,因此很昂贵。它也被用于性能分析,因为它使得堆栈展开变得简单。
2. DWARF调试信息(perf record --call-graph dwarf)。需要二进制文件构建时使用-g(-gline-tables-only)选项来添加DWARF调试信息。
3. Intel Last Branch Record (LBR)硬件特性(perf record --call-graph lbr)。与前两种方法相比,调用链不是很深。关于LBR的更多信息详见第6.2节。
DWARF如何发音:
DWARF在计算机领域中是一个非常常见的术语,它通常被读作[dwɔːrf],即“多夫”。其中“DW”代表“Debugging With”,而“ARF”则代表“Advanced Representation Format”。
以下是使用Linux perf收集程序中调用堆栈的示例。通过查看输出,我们可以知道55%的时间foo是从func1调用的。我们可以清楚地看到在foo的调用者之间的开销分布,并且现在可以将注意力集中在程序的CFG中最热门的路径上。

在使用Intel Vtune Profiler时,可以在配置分析时勾选相应的“Collect stacks”框来收集调用堆栈数据。在使用命令行界面时,可以指定-knob enable-stack-collection=true选项。
个人经验:了解收集调用堆栈数据的机制非常重要。我见过一些不熟悉这个概念的开发人员尝试使用调试器来获取这些信息。他们通过中断程序执行并分析调用堆栈(如“gdb”调试器中的“backtrace”命令)来完成此操作。开发人员应该让性能分析工具完成这项工作,这样更快速且提供更准确的数据。
iphone上有没有什么性能分析工具可以打印调用堆栈,而不是hook抓单点的调用堆栈?
在iPhone上,可以使用Instruments应用程序进行性能分析。通过选择“Time Profiler”Instrument并开始采样,可以收集整个应用程序的 CPU 使用情况,并捕获所有函数的调用堆栈。在Instruments中,可以将CPU使用率显示为函数级别,并且可以查看每个函数被调用的次数和执行时间。
另一个工具是 Xcode 的调试器,可以在其下方的“Debug Navigator”窗格中查看方法的调用堆栈。调试器可以捕获断点处的调用堆栈,也可以在代码执行时查看当前的调用堆栈。
这两个工具都可以在iPhone上打印完整的调用堆栈,而不是仅针对单个方法或函数。
介绍下iphone的Instruments应用程序:
Instruments是苹果公司为macOS和iOS开发的一款性能分析工具。它提供了一些内置的Instrument,用于检测CPU使用率、内存使用率、网络流量、能量消耗等等。其中,“Time Profiler” Instrument可以用于捕获 CPU 使用情况和检测应用程序中的性能瓶颈。
在Instruments中,可以选择要跟踪的应用程序,并使用所选 Instrument 进行采样。然后,工具会收集数据并生成可视化图表,以帮助开发人员分析应用程序的性能问题。图表主要分为两个部分:左侧是时间轴,右侧是函数列表。时间轴显示了应用程序的运行时间线,而函数列表则显示了被调用的函数及其所用的 CPU 时间。开发人员可以点击某一个函数来查看它的调用堆栈及其他相关信息。
除了内置的 Instrument 之外,Instruments 还支持自定义 Instrument。开发人员可以使用编程语言创建自己的 Instrument,以实现特定的性能分析需求。
总之,Instruments 是一个强大的性能分析工具,可以帮助开发人员诊断并解决应用程序中的性能问题。
iphone的Instruments应用程序哪里下载?
Instruments是Xcode自带的工具之一,在安装Xcode时会自动安装。若您的Mac上已经安装了Xcode,则可以在/Applications/Xcode.app/Contents/Applications/Instruments.app路径下找到Instruments应用程序。
如果您还没有安装Xcode,可以前往Apple Developer官网(https://developer.apple.com/downloads/)下载并安装最新版本的Xcode。请注意,在Xcode安装过程中,可能需要同意许可协议并输入管理员密码。安装完成后,您就可以找到Instruments应用程序并开始使用了。
5.4.4 Flame Graphs    //火焰图
一种流行的可视化性能分析数据和最常见代码路径的方法是使用火焰图。它可以让我们看到哪些函数调用占用了执行时间的大部分。图23展示了x264基准测试的火焰图示例。从上述火焰图中,我们可以看出占用最多执行时间的路径是x264 -> threadpool_thread_internal -> slices_write -> slice_write -> x264_8_macroblock_analyse。原始输出是交互式的,允许我们缩放到特定的代码路径。这个火焰图是由Brendan Gregg开发的开源脚本86生成的。还有其他能够生成火焰图的工具,也许KDAB Hotspot87是最流行的替代品。

Figure 23: A Flame Graph for x264 benchmark.
5.5 Rooflfline Performance Model    //屋顶线性能模型
Roofline Performance Model是在2009年由加州大学伯克利分校开发的一个以吞吐量为导向的性能模型,广泛应用于HPC领域。该模型中的“屋顶线”表达了这样一个事实:应用程序的性能不能超过计算机的能力。程序中的每个函数和每个循环都受到计算或存储容量的限制。如图24所示,这个概念被表示为一个应用程序的性能始终会受到某个“屋顶线”函数的限制。

Figure 24: Rooflfline model. © Image taken from NERSC Documentation.
//Arithmetic Intensity(算术强度)
//Attainable Flop/s(可达到的每秒浮点运算次数)
硬件有两个主要限制:它可以进行计算的速度(峰值计算性能,FLOPS)和它可以移动数据的速度(峰值内存带宽,GB/s)。应用程序的最大性能受到峰值FLOPS(水平线)和平台带宽乘以算术密度(对角线)之间的最小限制。
如图24中所示,Roofline图将两个应用程序A和B的性能与硬件限制进行了比较。程序中的不同部分可能具有不同的性能特点。Roofline模型考虑到了这一点,并允许在同一张图表上显示应用程序的多个函数和循环。
Listing 8 Naive parallel matrix multiplication.

1 void matmul(int N, float a[][2048], float b[][2048], float c[][2048]) {
2    #pragma omp parallel for
3    for(int i = 0; i < N; i++) {
4        for(int j = 0; j < N; j++) {
5            for(int k = 0; k < N; k++) {
6                c[i][j] = c[i][j] + a[i][k] * b[k][j];
7            }
8        }
9    }
10}

算术强度(AI)是一个FLOPS和字节数之间的比率,可以为程序中的每个循环提取。让我们计算清单8中代码的算术强度。在最内层的循环体中,我们有一次加法和一次乘法;因此,我们有2次FLOPS。此外,我们有三个读操作和一个写操作;因此,我们传输了4个操作*4个字节=16个字节。该代码的算术强度为2/16=0.125。AI作为给定性能点的X轴上的值。


Figure 25: Rooflfline model.
优化应用程序性能的传统方法是充分利用机器的SIMD和多核能力。通常情况下,我们需要优化许多方面:向量化、内存、线程。Roofline方法可以帮助评估应用程序的这些特性。在一个Roofline图表上,我们可以绘制标量单核、SIMD单核和SIMD多核性能的理论最大值(见图25)。这将使我们了解到提高应用程序性能的空间。如果我们发现我们的应用程序受到计算限制(即具有高算术强度)且低于峰值标量单核性能,我们应考虑强制向量化(参见第8.2.3节)并将工作分配到多个线程中。相反,如果应用程序的算术强度较低,则应寻求改进内存访问的方法(参见第8.1节)。使用Roofline模型优化性能的最终目标是将点向上移动。向量化和线程将点向上移动(应该是对应图25的向量化和多核CPU能力),通过增加算术强度优化内存访问将使点向右移动(FLOPS/字节数,这里是通过优化内存来减少字节数),并可能提高性能。
解释下SIMD和向量化的关系:
SIMD(单指令多数据)和向量化是密切相关的概念。简而言之,向量化是指将单个指令应用于多个数据元素,从而使计算机执行相同的操作时能够同时处理多个数据元素。而SIMD是实现向量化的硬件技术之一,它允许在一个CPU周期内同时处理多个数据元素,从而加速程序的执行。
具体来说,编写向量化代码意味着将循环中的操作组织成向量形式,即在一次指令中同时处理多个数据元素。这可以通过使用特定的指令集或库(如SSE、AVX等)来实现。而SIMD则提供了硬件支持,以便CPU可以在一次指令中并行处理多个数据,使向量化代码能够充分利用硬件加速,并实现更高效的程序执行。因此,向量化是一种技术方法,而SIMD是一种支持向量化的计算机硬件实现。
向量化code举例:
以下是一个将向量化应用于计算两个向量点积的示例C++代码:

```c++
#include <immintrin.h>


float dot_product(float *a, float *b, int n) {
    __m256 sum_vec = _mm256_setzero_ps();
    
    for (int i = 0; i < n; i += 8) {
        __m256 a_vec = _mm256_loadu_ps(&a[i]);
        __m256 b_vec = _mm256_loadu_ps(&b[i]);
        sum_vec = _mm256_add_ps(sum_vec, _mm256_mul_ps(a_vec, b_vec));
    }
    
    float sum = 0.0f;
    for (int i = 0; i < 8; i++) {
        sum += ((float*)&sum_vec)[i];
    }
    
    return sum;
}
```

这个函数利用了256位AVX向量指令集以每次处理8个单精度浮点数。在循环中,它使用_mm256_loadu_ps函数加载a和b向量中的8个元素,使用_mm256_mul_ps函数进行乘法操作,然后使用_mm256_add_ps函数计算它们的和。这些操作都在一个单独的向量寄存器中执行,从而加速点积计算。最后,函数将结果从向量寄存器中提取出来,并通过标量代码计算点积的最终值。
需要注意的是,当向量长度不是向量寄存器大小的倍数时,需要特殊处理。在上面的代码中,我们使用了一个for循环来处理余数部分,但这是比较低效的。一种更好的方法是使用向量掩码并使用_mm256_maskload_ps函数来加载未对齐的剩余元素。
向量化代码可以提高执行速度,并利用现代CPU中的SIMD指令以实现更高效的计算。
计算机的理论最大性能(roof lines)可以根据使用的机器的参数来进行估算。对于Intel Core i5-8259U处理器,使用AVX2指令集和2个Fused Multiply Add(FMA)单元(加乘运算)的单精度浮点峰值FLOPS(每秒浮点运算次数)可以按照以下公式进行计算:
Peak FLOPS = 8(逻辑核心数)× 256(AVX位宽)/ 32位(浮点数大小)× 2(FMA)× 3800 GHz(最大睿频)= 486.4 GFLOPs
该计算的结果为486.4 GFLOPs。这个数字表示了该处理器在理想情况下的最大性能,但实际上它可能会受到各种因素的影响而低于这个值,例如内存带宽、操作系统开销等等。然而,了解理论最大性能(roof lines)是一个非常有用的指标,因为它可以帮助程序员评估他们的代码是否已经充分利用了底层硬件的性能,并进行必要的优化。
睿频什么意思:
睿频(Turbo Boost)是一种由英特尔公司开发的技术,它可以使处理器在满负荷时自动提高时钟频率,从而提高性能。当处理器需要更高的性能时,睿频技术会自动增加时钟频率以达到更高的处理能力,当处理器不再需要更高的性能时,时钟频率则会自动降低以减少功耗和热量。这种技术可以让处理器更加智能化地运行,根据需要进行自适应调整,从而在保证性能的同时提高能效。可以通过BIOS设置或操作系统软件来控制睿频技术的启用和关闭,并且不同型号的处理器所支持的睿频倍频数不尽相同。
对于使用DDR4内存、2个内存通道,每次内存访问8个字节的Intel NUC Kit NUC8i5BEH,最大内存带宽可以按以下方式计算:
Peak Memory Bandwidth = 2400(DDR4内存传输速率)× 2(内存通道数)× 8(每次内存访问字节数)× 1(插座数)= 38.4 GiB/s
该计算的结果为38.4 GiB/s。这个数字表示了该设备在理想情况下的最大内存传输速率,但实际上它可能会受到各种因素的影响而低于这个值,例如内存延迟、内存排列方式等等。然而,了解设备的最大内存带宽是一个非常有用的指标,因为它可以帮助程序员评估和优化他们的代码中的内存访问模式并充分利用底层硬件的性能。
自动化工具如 Empirical Roofline Tool 和 Intel Advisor 可以通过运行一组准备好的基准测试来经验性地确定理论最大值。如果计算可以重用缓存中的数据,则可以实现更高的FLOP速率。 Roofline 可以通过为内存层次结构的每个级别引入专用 roofline 来考虑这一点(参见图26)。这些工具可以帮助程序员识别和消除代码中的内存瓶颈,从而充分利用底层硬件的性能,并优化代码以提高计算性能。使用这些工具需要一定的技术能力和经验,并且需要针对特定的硬件和应用程序进行调整和配置。
https://bitbucket.org/berkeleylab/cs-roofline-toolkit/src/master/

Figure 26: Rooflfline analysis for matrix multiplication on Intel NUC Kit NUC8i5BEH with 8GB RAM using clang 10 compiler.
在确定硬件限制后,我们可以开始评估应用程序相对于 Roofline 的性能。自动收集 Roofline 数据的两种最常用方法是采样(由 likwid 工具使用)和二进制插装(由 Intel Software Development Emulator (SDE) 使用)。采样产生较低的数据收集开销,而二进制插装提供更准确的结果。Intel Advisor 能够自动构建 Roofline 图表,甚至为特定循环的性能优化提供提示。图26展示了Intel Advisor生成的图表示例。请注意,Roofline 图表具有对数刻度。
https://github.com/RRZE-HPC/likwid
Roofline 方法允许通过在同一图表上打印“之前”和“之后”点来跟踪优化进度。因此,这是一个迭代的过程,可以指导开发人员使他们的应用程序充分利用硬件能力。图26反映了通过对清单8中的代码进行两次代码转换所获得的性能提升(图中红色圆框和红色方框):
• 交换两个最内层循环(交换第4行和第5行)。这样可以进行友好的缓存内存访问(见第8.1节)。    //cache-access优化
• 使用AVX2指令向量化最内层循环。    //向量化优化
//这两个优化都是使曲线向上移动
总之,Roofline 性能模型可帮助:
• 识别性能瓶颈。
• 指导软件优化。
• 确定何时完成优化。
• 评估相对于机器能力的性能。
Additional resources and links:
• NERSC Documentation, URL: https://docs.nersc.gov/development/performancedebugging-tools/roofline/.
• Lawrence Berkeley National Laboratory research, URL: https://crd.lbl.gov/departments/computer-science/par/research/rooflfline/
• Collection of video presentations about Rooflfline model and Intel Advisor, URL: https://techdecoded.intel.io/ (search “Rooflfline”).
• Perfplot is a collection of scripts and tools that allow a user to instrument performance counters on a recent Intel platform, measure them, and use the results to generate rooflfline and performance plots. URL: https://github.com/GeorgOfenbeck/perfplot
5.6 Static Performance Analysis    //静态性能分析
现在我们有广泛的静态代码分析工具。对于 C 和 C++ 语言,我们有 Clang 静态分析器、Klocwork、Cppcheck 等著名工具。它们旨在检查代码的正确性和语义。同样,还有一些工具尝试解决代码的性能方面问题。静态性能分析器不运行实际的代码。相反,它们模拟代码,就像在真正的硬件上执行一样。静态预测性能几乎是不可能的,因此这种类型的分析存在许多限制。首先,不可能对 C/C++ 代码进行性能静态分析,因为我们不知道它将被编译成的机器代码。因此,静态性能分析只能对汇编代码进行。
其次,静态分析工具模拟工作负载而不是执行它。显然非常慢,所以不能对整个程序进行静态分析。相反,工具会选择一些汇编代码片段,并尝试预测它在真实硬件上的行为。用户应该选择特定的汇编指令(通常是小循环)进行分析。因此,静态性能分析的范围非常狭窄。静态分析器的输出相当低级别,有时会将执行分解成 CPU 周期。通常,开发人员将其用于关键代码区域的细粒度调整,在那里每个周期都很重要。
5.6.1 Static vs. Dynamic Analyzers
静态工具不运行实际的代码,而是尝试模拟执行过程,并保留尽可能多的微架构细节。它们无法进行实际测量(如执行时间、性能计数器),因为它们不运行实际代码。其中的好处是您不需要真正的硬件,可以模拟不同 CPU 代的代码。另一个好处是您无需担心结果的一致性:与在真实硬件上执行相比,模拟(与执行)不会有任何偏差。静态工具的缺点是,它们通常无法预测和模拟现代 CPU 中的所有内容:它们基于一些可能存在错误和局限性的模型。静态性能分析器的例子包括 IACA 和 llvm-mca。
动态工具基于在真实硬件上运行代码并收集有关执行的各种信息。这是证明任何性能假设的唯一100%可靠的方法。作为缺点,通常需要特权访问权限才能收集低级性能数据,例如 PMCs。编写良好的基准测试并测量要测量的内容并不总是容易的。最后,您需要过滤噪声和各种副作用。动态性能分析器的例子包括 Linux perf、likwid 和 uarch-bench 等。可以在 easyperf 的博客上找到上述工具的使用和输出示例。这里还提供了用于静态和动态微架构性能分析的大量工具的集合。
个人经验是,我在需要探索一些有趣的 CPU 微架构效果时使用这些工具。静态和低级别的动态分析器(如 likwid 和 uarch-bench)允许我们在实践中观察硬件效果,同时进行性能实验。它们对于构建您对CPU工作方式的心理模型非常有帮助。
likwid是什么?
likwid是一个开源工具,用于在Linux系统上对多核处理器进行性能分析。使用likwid,您可以测量程序的总体性能、缓存性能和内存带宽使用情况,还可以度量各种硬件性能计数器(如PMC)。它还包括一个benchmarker,用于测试基准测试并比较多个不同实现之间的性能。likwid被广泛用于并行编程和高性能计算中,提供了许多有用的功能和API以进行性能分析。
likwid和simpleperf的区别是?
likwid和simpleperf都是用于在Linux系统上进行性能分析的工具,但它们之间存在一些区别。
首先,likwid和simpleperf的设计目标不同。likwid主要面向并行编程和高性能计算,提供了许多API和功能以进行硬件性能计数器的测量,并且可以自动确定可伸缩性瓶颈。而simpleperf则专注于提供一个易于使用的命令行界面,可以轻松地对C/C++、Java和Python等语言编写的应用程序进行基准测试和性能分析。
其次,两者的使用方式和输出格式也有所不同。使用likwid,您需要显式地在代码中插入测量指令,然后运行代码并读取输出文件。使用simpleperf,您只需在命令行中指定要分析的程序和选项,就可以获得详细的性能数据和结果。
最后,二者支持的硬件平台和操作系统也有所不同。虽然likwid可以在许多不同的处理器体系结构上运行,但主要针对x86和ARM架构。而simpleperf则适用于所有支持Linux内核性能事件子系统的处理器体系结构,包括x86、ARM、PowerPC和MIPS等。
因此,选择使用likwid还是simpleperf取决于您的具体需求和应用场景。如果您需要进行高级的硬件性能计数器测量和并行性分析,则likwid可能更适合您。如果您需要一个易于使用和直观的工具来快速进行性能分析,则simpleperf可能更适合您。
5.7 Compiler Optimization Reports
现今,软件开发非常依赖编译器进行性能优化。编译器在加速我们的软件方面发挥着极为重要的作用。通常情况下,开发者会把这一工作交给编译器,只有在发现编译器无法完成某些优化时才进行干预。可以说,这是一种不错的默认策略。为了更好地进行交互,编译器提供了优化报告,开发者可以用来进行性能分析。有时,开发者想知道某个函数是否被内联、循环是否被向量化、展开等等,如果它被展开了,展开因子是多少?这个信息可以通过查看生成的汇编指令来获得,但不是所有人都能轻易地阅读汇编语言,特别是函数很大、调用其他函数或者有很多循环被向量化,或者编译器创建了多个相同循环的版本时,这个任务就更难了。幸运的是,大多数编译器(包括GCC、ICC和Clang)都提供了优化报告,以便开发者检查特定代码片段所进行的优化。另一个编译器提供的提示案例是Intel® ISPC101编译器(请参见8.2.3.7节),它会针对编译为相对低效代码的代码结构发出大量性能警告。示例9展示了一个由Clang 6.0没有向量化的循环。要在Clang中生成优化报告,需要使用-Rpass*标志:

Listing 9 a.c

1 void foo(float* __restrict__ a,
2          float* __restrict__ b,
3          float* __restrict__ c,
4          unsigned N) {
5    for (unsigned i = 1; i < N; i++) {
6    a[i] = c[i-1]; // value is carried over from previous iteration
7    c[i] = b[i];
8    }
9 }

通过检查上述优化报告,我们可以看到该循环没有被向量化,而是被展开了。对于开发者来说,有时很难识别出示例9中第5行循环中的向量依赖关系。c[i-1]加载的值依赖于前一次迭代的存储(请参见图27中的操作#2和#3)。通过手动展开前几次迭代,可以揭示这种依赖性:

// iteration 1
a[1] = c[0];
c[1] = b[1]; // writing the value to c[1]
// iteration 2
a[2] = c[1]; // reading the value of c[1]
c[2] = b[2];
...

Figure 27: Visualizing the order of operations in Listing 9.
如果我们要将示例9中的代码向量化,将会导致在数组a中写入错误的值。假设CPU的SIMD单元可以一次处理四个浮点数,我们将得到可以用以下伪代码表示的代码:

// iteration 1
a[1..4] = c[0..3]; // oops, a[2..4] get the wrong values
c[1..4] = b[1..4];
...

示例9中的代码无法进行向量化,因为循环内部的操作顺序很重要。可以通过交换第6行和第7行来修复此示例,而不改变函数的语义,如示例10所示。有关发现向量化机会以及使用编译器优化报告的示例的更多信息,请参见8.2.3节。
Listing 10 a.c

1 void foo(float* __restrict__ a,
2    float* __restrict__ b,
3    float* __restrict__ c,
4    unsigned N) {
5    for (unsigned i = 1; i < N; i++) {
6    c[i] = b[i];
7    a[i] = c[i-1];
8    }
9 }

在优化报告中,我们可以看到该循环现在已被向量化:

编译器报告是针对每个源文件生成的,这可能很大。用户可以在输出中简单地搜索感兴趣的源代码行。基于LLVM的编译器的Compiler Explorer网站提供了“优化输出”工具,在你将鼠标悬停在相应的源代码行上时会报告所执行的转换。在LTO模式下,一些优化是在链接阶段进行的。要从编译和链接阶段发出编译器报告,应向编译器和链接器传递专用选项。有关更多信息,请参阅LLVM “Remarks”指南。编译器优化报告不仅有助于发现错过的优化机会并解释其原因,而且还对测试假设很有用。编译器通常根据其成本模型分析决定某个特定转换是否有益。但它并不总是做出最优选择,我们可以进一步调整。可以通过使用#pragma、属性、编译器内建函数等,在报告中检测缺少的优化并向编译器提供提示。请参见easyperf博客上使用此类提示的示例。像往常一样,在实际环境中测量并验证您的假设。
个人经验:编译器优化报告可能是您工具箱中的关键项目之一。这是一种快速检查特定热点进行了哪些优化、是否有一些重要的优化失败的方法。我使用优化报告发现了许多改进机会。
5.8 Chapter Summary
• 延迟和吞吐量通常是程序性能的最终指标。在寻找改进它们的方法时,我们需要获取更详细的关于应用程序执行的信息。硬件和软件都提供可用于性能监控的数据。
• 代码仪表化允许我们跟踪程序中的许多内容,但会在开发和运行时两方面带来相对较大的开销。虽然现在开发人员很少手动仪表化他们的代码,但这种方法仍然适用于自动化流程,例如PGO。
• 跟踪在概念上类似于仪表化,对于探索系统中的异常情况非常有用。跟踪允许我们捕获带有时间戳的每个事件的完整事件序列。
• 工作负载特征化是一种比较和分组应用程序基于他们的运行时行为的方法。一旦特征化,可以遵循特定的配方来查找程序中的优化空间。
• 抽样跳过程序执行的大部分,只取一个样本来代表整个时间间隔。虽然抽样通常可以给出足够准确的分布,但最为广为人知的用例是在代码中查找热点。由于不需要重新编译程序并且运行时开销非常小,因此抽样是最受欢迎的分析方法。
• 一般来说,计数和抽样的运行时开销非常低(通常低于2%)。一旦开始在不同事件之间进行多路复用,计数将变得更加昂贵(5-15%开销),随着抽样频率的增加,抽样将变得更加昂贵[Nowak and Bitzes, 2014]。在分析长时间运行的工作负载或者不需要非常精确的数据时,考虑使用用户模式抽样。
• 屋顶线是一个面向吞吐量的性能模型,在HPC领域被广泛使用。它允许绘制应用程序的性能与硬件限制之间的关系。屋顶线模型有助于确定性能瓶颈,指导软件优化并跟踪优化进展。
• 有一些工具尝试静态分析代码的性能。这些工具模拟代码片段而不是运行它。这种方法有许多限制和约束,但是可以得到非常详细和低级的报告。
• 编译器优化报告有助于找到缺失的编译器优化。它也可以指导开发人员组合新的性能实验。
//HPC:高性能计算。这本书很多东西涉及到优化code细节,属于HPC领域的技能

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值