找回密码
 立即注册
首页 业界区 安全 现代CPU调优4性能分析中的术语和指标

现代CPU调优4性能分析中的术语和指标

璋锌 2025-6-1 19:04:18
4 性能分析中的术语和指标

与许多工程学科一样,性能分析也大量使用特殊的术语和指标。对于初学者来说,查看 Linux perf 或 Intel VTune Profiler 等分析工具生成的配置文件可能会非常困难。这些工具使用了许多复杂的术语和指标,但是,如果你打算从事任何严肃的性能工程工作,这些指标都是 “必须知道的”。
既然我提到了 Linux perf,那就让我简单介绍一下这个工具,因为我在本章和后面的章节中有很多使用它的例子。Linux perf 是一个性能剖析器,你可以用它来查找程序中的热点、收集各种低级 CPU 性能事件、分析调用堆栈以及其他许多事情。我将在本书中大量使用 Linux perf,因为它是最流行的性能分析工具之一。我喜欢展示 Linux perf 的另一个原因是它是开源软件,这使热心读者能够探索现代剖析工具内部的机制。这对学习本书中介绍的概念尤其有用,因为基于图形用户界面的工具(如 Intel® VTune™ Profiler)往往会隐藏所有的复杂性。我们将在第7章对 Linux perf 进行更详细的介绍。
本章将简要介绍性能分析中使用的基本术语和指标。我们将首先定义一些基本术语,如已退休/已执行指令、IPC/CPI、µOPS、内核/参考时钟、缓存未命中和分支预测错误。
然后,我们将了解如何测量系统的内存延迟和带宽,并介绍一些更高级的指标。最后,我们将对四个行业工作负载进行基准测试,并查看收集到的指标。
4.1 已退休指令与已执行指令


  • 已退休指令 (retired instructions):那些已经完成执行并被确认的指令。这些指令是程序真正需要的指令,它们的结果已经被写入到寄存器或内存中。简单来说,就是程序逻辑真正执行的指令。

    • 是程序逻辑的真实反映。
    • 不包括推测执行(speculative execution)中被取消的指令。
    • 可以用来计算程序的实际执行时间。

  • 已执行指令 (executed instructions): 所有被CPU执行过的指令,包括那些推测执行的指令,即使最终这些指令被取消了。也就是说,CPU尝试执行过的指令,无论结果是否最终被采用。

    • 包括了所有被执行过的指令,无论它们是否最终被确认。
    • 可以用来衡量CPU的工作负载。
    • 可能比retired instructions数量更多,因为现代CPU通常会进行推测执行。

现代处理器执行的指令通常多于程序流程所需的指令。出现这种情况的原因是某些指令是预测执行的,第 3.3.3 节对此进行了讨论。对于大多数指令,CPU 会在结果可用时立即提交,而之前的所有指令都已退休。但对于推测执行的指令,CPU 会保留其结果,而不会立即提交结果。当推测结果正确时,CPU 会解锁这些指令,并照常执行。但如果推测结果是错误的,CPU 就会丢弃推测指令所做的所有更改,而不会将其报废。因此,CPU 处理的指令可以被执行,但不一定会退休。考虑到这一点,我们通常可以预期执行指令的数量会高于退役指令的数量。
但也有例外。某些指令被视为惯用指令,无需实际执行即可解决。例如第 3.8.2 节中讨论的 NOP、移动消除和清零。这类指令不需要执行单元,但仍会被退休。因此,从理论上讲,可能会出现退役指令数多于执行指令数的情况。
在大多数现代处理器中,都有一个性能监控计数器(PMC:performance monitoring counter)来收集退休指令的数量。目前还没有收集已执行指令的性能事件,不过有一种方法可以收集已执行和已报废的 µops 指令,我们很快就会看到。通过运行 Linux perf,可以轻松获得退休指令的数量:
  1. $ perf stat -e instructions -- ls
  2. code  my.script  openpbs-23.06.06  slurm-8.out  snap  v23.06.06
  3. Performance counter stats for 'ls':
  4.          1,991,109      instructions
  5.        0.002156994 seconds time elapsed
  6.        0.000000000 seconds user
  7.        0.002250000 seconds sys
复制代码
4.2 CPU 利用率

CPU 利用率是指一段时间内核忙的时间百分比。从技术上讲,当 CPU 没有运行内核idle线程时,它就被认为已被利用:CPU Utilization=CPU_CLK_UNHALTED.REF_TSC/TSC。
其中 CPU_CLK_UNHALTED.REF_TSC 计算内核未处于停止状态时的参考周期数。TSC 代表时间戳计数器(将在第 2.5 节中讨论),它总是滴答作响。
如果 CPU 利用率较低,通常意味着应用程序性能较差,因为 CPU 浪费了部分时间。不过,CPU 利用率高并不总是性能好的表现。它只是表明系统正在做一些工作,但并没有说明它在做什么:CPU 的利用率可能很高,即使它停滞在等待内存访问。在多线程环境下,线程也可能在等待资源的同时继续运行。稍后,我们将在第 13.1 节讨论并行效率指标,尤其是过滤旋转时间的 “CPU 有效利用率”。
Linux perf 会自动计算系统中所有 CPU 的 CPU 利用率:
  1. # perf stat  -- ls
  2. code  my.script  openpbs-23.06.06  slurm-8.out  snap  v23.06.06
  3. Performance counter stats for 'ls':
  4.               0.82 msec task-clock                       #    0.622 CPUs utilized
  5.                  0      context-switches                 #    0.000 /sec
  6.                  0      cpu-migrations                   #    0.000 /sec
  7.                 93      page-faults                      #  113.459 K/sec
  8.          1,762,015      cycles                           #    2.150 GHz
  9.          2,005,311      instructions                     #    1.14  insn per cycle
  10.            371,410      branches                         #  453.116 M/sec
  11.             11,882      branch-misses                    #    3.20% of all branches
  12.                         TopdownL1                 #     24.4 %  tma_backend_bound
  13.                                                   #     11.8 %  tma_bad_speculation
  14.                                                   #     38.7 %  tma_frontend_bound
  15.                                                   #     25.1 %  tma_retiring
  16.        0.001318194 seconds time elapsed
  17.        0.000000000 seconds user
  18.        0.001422000 seconds sys
复制代码
4.3 CPI 和 IPC

这是两个基本指标,分别代表

  • 每周期指令数 (IPC Instructions Per Cycle):平均每个周期有多少指令退休:IPC = INST_RETIRED.ANY / CPU_CLK_UNHALTED.THREAD,其中 INST_RETIRED.ANY 计算退休指令数,CPU_CLK_UNHALTED.THREAD 计算线程未处于停止状态时的内核周期数。即:IPC = 总退休指令数 / 总时钟周期数

    • 计算公式: IPC = 总指令数 / 总时钟周期数
    • IPC越高,表示CPU的并行处理能力越强。
    • IPC是CPI的倒数,即 IPC = 1 / CPI。
    • 理论上,理想的IPC值为1,表示每个时钟周期都可以执行一条指令。但实际上,由于各种因素的限制,IPC值通常小于1。

  • 每指令周期(CPI Cycles Per Instruction):CPI表示CPU执行一条指令所需的平均时钟周期数。简单来说,就是执行一条指令需要“多少个节拍”。

    • 计算公式: CPI = 总时钟周期数 / 总退休指令数。
    • CPI越低,表示CPU执行指令的效率越高。
    • 不同的指令有不同的CPI,例如,简单的算术运算指令可能只需要一个时钟周期,而复杂的浮点运算指令可能需要多个时钟周期。
    • CPI受到多种因素的影响,包括CPU微架构、指令类型、程序特性等。

我更倾向于使用 IPC,因为它更易于比较。使用 IPC 时,我们希望每个周期尽可能多地执行指令,因此 IPC 越高越好。而 CPI 则恰恰相反:我们希望每条指令的周期越少越好,因此 CPI 越低越好。使用 “越高越好 ”的指标进行比较更简单,因为你不必每次都进行思维倒置。
IPC 与 CPU 时钟频率之间的关系非常有趣。从广义上讲,性能 = 工作/时间,其中我们可以用指令数来表示工作,用秒来表示时间。程序运行的秒数可以用总周期/频率来计算:
性能 = 指令数 × 频率 / 周期 = IPC × 频率 我们可以看到,性能与 IPC 和频率成正比。如果我们提高这两个指标中的任何一个,程序的性能就会提高。
从基准测试的角度来看,IPC 和频率是两个独立的指标。
我看到一些工程师错误地将它们混为一谈,认为如果提高频率,IPC 也会随之提高。但事实并非如此。如果将处理器的频率从 5 GHz 提高到 1 GHz,在许多应用中仍能获得相同的 IPC。
不过,频率只能告诉我们单个时钟周期的速度,而 IPC 则计算每个周期做了多少工作。因此,从基准测试的角度来看,IPC 完全取决于处理器的设计,与频率无关。非顺序内核的 IPC 通常比顺序内核高得多。当你增加 CPU 高速缓存的大小或改进分支预测时,IPC 通常会上升。
现在,如果你去问硬件架构师,他们肯定会告诉你 IPC 与频率之间存在依赖关系。从 CPU 设计的角度来看,你可以故意降低处理器的频率,这样会延长每个周期的时间,并有可能在每个周期中挤出更多的工作。最终,你将获得更高的 IPC,但频率却更低。硬件供应商以不同的方式处理这一性能等式。例如,英特尔和 AMD 芯片的频率通常很高,最近推出的英特尔 13900KS 处理器开箱即可提供 6 GHz 的涡轮频率,无需超频。另一方面,苹果 M1/M2 芯片的频率较低,但 IPC 却较高。较低的频率有利于降低功耗。另一方面,更高的 IPC 通常需要更复杂的设计、更多的晶体管和更大的芯片尺寸。在此,我们将不再赘述所有的设计权衡,因为这些是另一本书的主题。
IPC 对于评估硬件和软件效率都很有用。硬件工程师使用这一指标来比较不同厂商的 CPU 代次和 CPU。当降低 CPU 频率时,内存速度相对于 CPU 会更快。这可能会掩盖实际的内存瓶颈,人为地提高 IPC。
IPC 是衡量 CPU 微体系结构性能的标准,工程师和媒体用它来表示相对于上一代产品的性能提升。不过,要进行公平的比较,需要两个系统以相同的频率运行。
IPC 也是评估软件的有用指标。它能让你直观地了解应用程序中的指令在 CPU 管线中的运行速度。在本章后面,您将看到几个具有不同 IPC 的生产应用程序。内存密集型应用程序的 IPC 通常较低(0-1),而计算密集型工作负载的 IPC 通常较高(4-6)。
Linux perf 用户可以通过运行以下命令来测量其工作负载的 IPC:
  1. # perf stat -e cycles,instructions -- ls
  2. code  my.script  openpbs-23.06.06  slurm-8.out  snap  v23.06.06
  3. Performance counter stats for 'ls':
  4.          1,895,107      cycles
  5.          1,980,477      instructions                     #    1.05  insn per cycle
  6.        0.000726477 seconds time elapsed
  7.        0.000766000 seconds user
  8.        0.000000000 seconds sys
  9. # 或 # perf stat -- ls
复制代码
4.4 微操作(Micro-operations)

x86 架构的微处理器将复杂的 CISC 指令转化为简单的 RISC 微操作,简称 µop。ADD rax, rbx 这样的简单寄存器到寄存器加法指令只产生一个 µop,而 ADD rax, [mem] 这样的复杂指令则可能产生两个 µop:一个用于从 mem 内存位置加载到临时(未命名)寄存器,另一个用于将其添加到 rax 寄存器。指令 ADD [mem], rax 产生三个 µops:一个用于从内存加载,一个用于加法,一个用于将结果存储回内存。
将指令拆分为微操作的主要优势在于 µops 可以被执行:

  • 乱序执行
考虑 PUSH rbx 指令,该指令将堆栈指针递减 8 个字节,然后将源操作数存储到堆栈顶部。假设 PUSH rbx 在解码后被 “破解 ”为两个从属的微操作:
  1. SUB rsp, 8
  2. STORE [rsp], rbx
复制代码
通常情况下,函数序幕会通过使用多条 PUSH 指令来保存多个寄存器。在我们的例子中,下一条 PUSH 指令可以在上一条 PUSH 指令的 SUB µop 结束后开始执行,而不必等待 STORE µop,因为 STORE µop 现在可以异步执行。

  • 并行:考虑 HADDPD xmm1, xmm2 指令,该指令将 xmm1 和 xmm2 中的两个双精度浮点数值相加(还原),并将两个结果存储到 xmm1 中,如下所示:
  1. xmm1[63:0] = xmm2[127:64] + xmm2[63:0]
  2. xmm1[127:64] = xmm1[127:64] + xmm1[63:0]
复制代码
对这条指令进行微编码的一种方法如下: 1) 缩减 xmm2 并将结果存储到 xmm_tmp1[63:0],2) 缩减 xmm1 并将结果存储到 xmm_tmp2[63:0],3) 将 xmm_tmp1 和 xmm_tmp2 合并到 xmm1。总共执行三个 µOP。注意步骤 1) 和 2) 是独立的,因此可以并行执行。
尽管我们刚才讨论的是如何将指令拆分成更小的部分,但有时也可以将 µops 融合在一起。现代 x86 CPU 有两种融合方式:

  • 微融合:将来自同一机器指令的 µops 融合在一起。微融合只适用于两种组合:内存写入操作和读取修改操作。例如:add eax, [mem]
    这条指令中有两个 µops 1) 读取内存位置 mem,和 2)将其添加到 eax 中。通过微融合,两个 µops 在解码步骤中融合为一个。
  • 宏融合
融合来自不同机器指令的 µ操作。在某些情况下,解码器可以将算术或逻辑指令与随后的条件跳转指令融合为一个单一的计算和分支 µop 。例如
  1. .loop:
  2.   dec rdi
  3.   jnz .loop
复制代码
通过宏融合,DEC 和 JNZ 指令中的两个 µop 融合为一个。Zen4 微体系结构还增加了对 DIV/IDIV 和 NOP 宏融合的支持[Advanced Micro Devices,2023,第 2.9.4 和 2.9.5 节](https://www.amd.com/content/dam/amd/en/documents/epyc-technical-docs/software-optimization-guides/57647.zip)。
微融合和宏融合在从解码到退出的流水线各个阶段都能节省带宽。融合操作共享重排缓冲区(ROB)中的一个条目。当融合 µop 只使用一个条目时,ROB 的容量能得到更好的利用。这种融合的 ROB 条目随后会被分派到两个不同的执行端口,但会作为一个单元再次退役。读者可以在Fog, 2023a中了解有关 µop 融合的更多信息。
要收集一个应用程序的已发布、已执行和已退役 µop 的数量,可以使用 Linux perf,如下所示:
  1. # perf stat -e uops_issued.any,uops_executed.thread,uops_retired.slots -- ls
  2. code  my.script  openpbs-23.06.06  slurm-8.out  snap  v23.06.06
  3. Performance counter stats for 'ls':
  4.          2,948,879      uops_issued.any
  5.          2,667,937      uops_executed.thread
  6.          2,218,621      uops_retired.slots
  7.        0.000702776 seconds time elapsed
  8.        0.000000000 seconds user
  9.        0.000840000 seconds sys
复制代码
各代 CPU 将指令拆分为微操作的方式可能有所不同。通常,一条指令使用的 µops 数量越少,说明硬件对该指令的支持越好,延时越短,吞吐量越高。对于最新的英特尔和 AMD CPU,绝大多数指令只产生一个 µop。有关最新微体系结构 x86 指令的延迟、吞吐量、端口使用和 µop 数量,请访问 uops.info62 网站。
4.5 Pipeline Slot(流水线槽)

一些性能工具使用的另一个重要指标是管道槽的概念。流水线槽表示处理一个 µop 所需的硬件资源。
下图展示了 CPU 的执行流水线,每个周期有 4 个分配槽。这意味着内核每个周期可以为 4 个新的 µOP 分配执行资源(重命名为源和目标寄存器、执行端口、ROB 条目等)。
这样的处理器通常称为 4 宽机器。在图中的连续 6 个周期中,只有一半的可用插槽被使用(用黄色标出)。从微体系结构的角度来看,执行这种代码的效率只有 50%。
1.png

英特尔的 Skylake 和 AMD Zen3 内核采用 4 宽分配。英特尔的 Sunny Cove 微架构是 5 宽设计。截至 2023 年底,最新的 Golden Cove 和 Zen4 架构均采用 6 宽分配。苹果 M1 和 M2 设计为 8 宽,苹果 M3 为 9-µop 执行带宽,见[苹果,2024 年,表 4.10](https://developer.apple.com/documentation/apple-silicon/cpu-optimization-guide)。
机器的宽度为 IPC 设置了上限。例如,当你的计算显示 Golden Cove 内核的 IPC 超过 6 时,你就应该怀疑了。
只有极少数应用能达到机器的最大 IPC。例如,英特尔 Golden Cove 内核理论上每个时钟可执行 4 个整数加法/减法,外加 1 个加载,外加 1 个存储(共 6 个指令),但应用程序不太可能拥有相邻独立指令的适当组合,以利用所有潜在并行性。
流水线槽利用率是自顶向下微体系结构分析的核心指标之一(参见第 6.1 节)。例如,“前端约束 ”和 “后端约束 ”指标用各种瓶颈导致的未利用流水线槽的百分比表示。
4.6 内核周期与参考周期

大多数 CPU 采用时钟信号来控制其顺序操作的节奏。时钟信号由外部发生器产生,每秒提供一致的脉冲数。时钟脉冲的频率决定了 CPU 执行指令的速度。因此,时钟越快,CPU 每秒执行的指令就越多。
大多数现代 CPU(包括英特尔和 AMD CPU)都没有固定的工作频率。相反,它们实现了动态频率缩放。例如,宏融合比较和分支指令只需要一个流水线插槽,但被算作两条指令。在某些极端情况下,这可能导致 IPC 大于机器宽度。在 Intel CPU 中称为 Turbo Boost,在 AMD 处理器中称为 Turbo Core。它能使 CPU 动态地提高或降低频率。降低频率可降低功耗,但会牺牲性能;提高频率可提高性能,但会牺牲功耗。
内核时钟周期计数器以 CPU 内核运行的实际频率计算时钟周期。参考时钟事件则是以处理器运行的基准频率来计算时钟周期。让我们看看在运行单线程应用程序的 Skylake i7-6000 处理器上进行的实验,该处理器的基本频率为 3.4 GHz:
  1. $ perf stat -e cycles,ref-cycles ./a.exe
  2.   43340884632  cycles        # 3.97 GHz
  3.   37028245322  ref-cycles    # 3.39 GHz
  4.       10.899462364 seconds time elapsed
复制代码
平台上的外部时钟频率为 100 MHz,如果我们用时钟乘数对其进行缩放,就能得到处理器的基本频率。Skylake i7-6000 处理器的时钟乘数等于34:这意味着当 CPU 以基准频率(即 3.4 GHz)运行时,每一个外部脉冲将执行 34 个内部周期。
周期事件计算 CPU 的实际周期并考虑频率缩放。利用上述公式,我们可以确认平均工作频率为 43340884632 个周期/10.899 秒 = 3.97 GHz。在比较一小段代码两个版本的性能时,以时钟周期为单位测量时间比以纳秒为单位更好,因为这样可以避免时钟频率上下波动的问题。
4.7 Cache Miss(缓存未命中)

如第 3.6 节所述,特定级别缓存中缺失的任何内存请求都必须由更高级别的缓存或 DRAM 来处理。这意味着此类内存访问的延迟会大大增加。内存子系统组件的典型延迟如表所示。当内存请求在最后一级高速缓存(LLC)中出现错误并一直向下进入主内存时,性能将大受影响64: 基于 x86 平台的内存子系统的典型延迟。
2.png

指令和数据读取都有可能在高速缓存中失效。根据 “自顶向下微体系结构分析”(参见第 6.1 节),指令缓存(I-cache)未命中被描述为前端失速,而数据缓存(D-cache)未命中被描述为后端失速。指令缓存未命中发生在 CPU 流水线的早期,即指令获取期间。数据缓存未命中则发生在指令执行阶段的更晚些时候。
Linux perf 用户可以通过运行以下命令来收集 L1 缓存未命中次数:
  1. # perf stat -e mem_load_retired.fb_hit,mem_load_retired.l1_miss,mem_load_retired.l1_hit,mem_inst_retired.all_loads -- ls
  2. code  my.script  openpbs-23.06.06  slurm-8.out  snap  test.sh  t.out  v23.06.06
  3. Performance counter stats for 'ls':
  4.             10,707      mem_load_retired.fb_hit
  5.              8,884      mem_load_retired.l1_miss
  6.            473,776      mem_load_retired.l1_hit
  7.            493,197      mem_inst_retired.all_loads
  8.        0.000797134 seconds time elapsed
  9.        0.000000000 seconds user
  10.        0.000910000 seconds sys
复制代码
以上是 L1 数据高速缓存和填充缓冲区的所有负载明细。负载既可能命中已分配的填充缓冲区(fb_hit),也可能命中 L1 缓存(l1_hit),或者两者都未命中(l1_miss),因此 all_loads = fb_hit + l1_hit + l1_miss。
我们可以进一步细分 L1 数据未命中情况,并通过运行以下命令分析 L2 缓存行为:
  1. # perf stat -e     mem_load_retired.l1_miss,mem_load_retired.l2_hit,mem_load_retired.l2_miss -- ls
  2. code  my.script  openpbs-23.06.06  slurm-8.out  snap  test.sh  t.out  v23.06.06
  3. Performance counter stats for 'ls':
  4.              8,911      mem_load_retired.l1_miss
  5.              6,862      mem_load_retired.l2_hit
  6.              2,067      mem_load_retired.l2_miss
  7.        0.000760850 seconds time elapsed
  8.        0.000000000 seconds user
  9.        0.000825000 seconds sys
复制代码
从这个例子中,我们可以看到 23% 在 L1 D 缓存中未命中的负载也在 L2 缓存中未命中,因此 L2 命中率为 77%。对 L3 缓存也可以进行类似的细分。
4.8 错误预测分支(Mispredicted Branch)

现代 CPU 会尝试预测条件分支指令的结果(执行或不执行)。例如,当处理器看到如下代码时:
  1. dec eax
  2. jz .zero
  3. # eax 不为 0
  4. ...
  5. zero:
  6. # eax 为 0
复制代码
在上述示例中,jz 指令是一条条件分支指令。为了提高性能,现代处理器在每次看到分支指令时都会尝试猜测结果。这就是我们在第 3.3.3 节中讨论过的 “预测执行”。例如,处理器会猜测不会执行分支指令,并执行与 eax 不为 0 的情况相对应的代码。但是,如果猜测错误,这就叫做分支错误预测,CPU 需要撤销最近所做的所有推测工作。
错误预测分支通常会带来 10 到 25 个时钟周期的惩罚。首先,所有根据错误预测获取并执行的指令都需要从流水线中清除。之后,一些缓冲区可能需要清理,以恢复错误推测开始时的状态。
Linux perf 用户可以通过运行以下命令检查分支错误预测的数量
  1. # perf stat -e branches,branch-misses -- ls
  2. code  my.script  openpbs-23.06.06  slurm-8.out  snap  test.sh  t.out  v23.06.06
  3. Performance counter stats for 'ls':
  4.            368,465      branches
  5.             11,818      branch-misses                    #    3.21% of all branches
  6.        0.000705814 seconds time elapsed
  7.        0.000793000 seconds user
  8.        0.000000000 seconds sys
  9. # 或 perf stat -- ls
复制代码
4.9 性能指标(Performance Metrics)

能够收集各种性能事件对性能分析非常有帮助。不过,有一个注意事项。比方说,你运行了一个程序,并收集了 MEM_LOAD_RETIRED.L3_MISS 事件,该事件计算 LLC 错失的次数,结果显示数值为 10 亿。当然,这听起来好像很多,所以你决定调查一下这些缓存未命中是从哪里来的。错了!你确定这是一个问题吗?如果一个程序只进行了 20 亿次加载,那么是的,这是一个问题,因为有一半的加载在 LLC 中未命中。相反,如果一个程序有一万亿次加载,那么只有千分之一的加载会导致 L3 缓存缺失。
这就是为什么除了硬件性能事件外,性能工程师还经常使用建立在原始事件基础上的指标。下表列出了英特尔第 12 代 Golden Cove 架构的指标列表以及说明和公式。该列表并不详尽,但显示了最重要的指标。英特尔 CPU 的完整指标列表及其计算公式请参见 TMA_metrics.xlsx。
3.png

关于这些指标的几点说明。首先,ILP 和 MLP 指标并不代表应用程序的理论最大值;相反,它们衡量的是特定机器上应用程序的实际 ILP 和 MLP。在资源无限的理想机器上,这些数字会更高。其次,除了 “DRAM BW 使用量 ”和 “负载缺失实际延迟 ”外,所有指标都是分数;我们可以对每个指标进行相当直观的推理,以判断特定指标是高还是低。但是,要理解 “DRAM BW 使用情况 ”和 “负载缺失实际延迟 ”指标,我们需要将它们与上下文联系起来。对于前者,我们想知道程序是否使内存带宽达到饱和。对于后者,我们需要知道程序是否达到了内存带宽饱和;而对于后者,我们需要知道缓存缺失的平均成本,除非我们知道缓存层次结构中每个组件的延迟时间,否则这本身毫无用处。我们将在下一节讨论如何找出缓存延迟和峰值内存带宽。
有些工具可以自动报告性能指标。如果不能,您也可以手动计算这些指标,因为您知道必须收集的公式和相应的性能事件。上表提供了英特尔 Golden Cove 架构的计算公式,但只要基础性能事件可用,您也可以在其他平台上建立类似的指标。
4.10 内存延迟和带宽

在现代环境中,低效内存访问通常是主要的性能瓶颈。
因此,处理器从内存子系统获取数据的速度是决定应用性能的关键因素。内存性能包括两个方面:1)CPU 从内存获取单个字节的速度(延迟);2)每秒可获取的字节数(带宽)。两者在各种情况下都很重要,我们稍后将举例说明。在本节中,我们将重点测量内存子系统组件的峰值性能。
英特尔内存延迟检查器(MLC) 是对 x86 平台有帮助的工具之一,该工具在 Windows 和 Linux 上免费提供。MLC 可以使用不同的访问模式和负载测量高速缓存和内存的延迟和带宽。在基于 ARM 的系统上没有类似的工具,但是用户可以从资源中下载并构建内存延迟和带宽基准。
此类项目的例子有 lmbench68、bandwidth和 Stream。
我们将只关注指标的一个子集,即空闲读取延迟和读取带宽。让我们从读取延迟开始。空闲是指在我们进行测量时,系统处于空闲状态。这将为我们提供从内存系统组件中获取数据所需的最短时间,但当系统被其他 “内存饥渴 ”的应用加载时,访问延迟会随着其他 “内存饥渴 ”应用的增加而增加。但当系统被其他 “内存饥渴 ”应用加载时,访问延迟会增加,因为在不同点上可能会有更多的资源排队等待。MLC 通过执行依赖加载(也称为指针追逐)来测量空闲延迟。测量线程会分配一个非常大的缓冲区,并对其进行初始化,使缓冲区内的每个(64 字节)缓存行都包含一个指向缓冲区内另一个非相邻缓存行的指针。通过适当调整缓冲区的大小,我们可以确保几乎所有的负载都会到达缓存或主存储器的某个级别。
我的测试系统是一台英特尔 Alder Lake 电脑,配备酷睿 i7-1260P 处理器和 16GB DDR4 @ 2400 MT/s 双通道内存。该处理器具有 4P(性能)超线程和 8E(高效)内核。每个 P 核有 48 KB 的一级数据缓存和 1.25 MB 的二级缓存。每个 E 核有 32 KB 的 L1 数据高速缓存,四个 E 核组成一个集群,可访问共享的 2 MB L2 高速缓存。系统中的所有内核都有 18 MB L3 高速缓存支持。如果使用 10 MB 的缓冲区,我们几乎可以肯定,对该缓冲区的重复访问将在 L2 中失误,但在 L3 中命中。下面是 mlc 命令的示例:
  1. $ sudo ./mlc --idle_latency -c0 -L -b10m
  2. Intel(R) Memory Latency Checker - v3.10
  3. Command line parameters: --idle_latency -c0 -L -b10m
  4. Using buffer size of 10.000MiB
  5. Each iteration took 31.1 base frequency clocks (12.5 ns)
复制代码
此外,MLC 还有 --loaded_latency 选项,用于测量其他线程产生内存流量时的延迟。选项 -c0 将测量线程引向 逻辑 CPU 0(P 核)。在我们的测量中,选项 -L 支持超大页面,以限制 TLB 的影响。选项 -b10m 可让 MLC 使用 10MB 的缓冲区,适合我们系统的 L3 缓存。
4.png

上图显示了 L1、L2 和 L3 缓存的读取延迟,使用 MLC 工具测量,启用超大页面。。图中有四个不同的区域。左侧第一个区域从 1 KB 到 48 KB 的缓冲区大小对应于 L1 D 缓存,它是每个物理内核的私有缓存。我们可以看到 E 核的延迟为 0.9 ns,P 核略高,为 1.1 ns。此外,我们还可以使用此图确认缓存大小。请注意,缓冲区大小超过 32 KB 后,E 核的延迟开始攀升,但 P 核的延迟在 48 KB 之前保持不变。
这证明 E 核的 L1 D 缓存大小为 32 KB,而 P 核为 48 KB。
第二个区域显示的是二级缓存延迟,E 核的延迟几乎是 P 核的两倍(5.9 ns 对 3.2 ns)。对于 P 核,在缓冲区大小超过 1.25 MB 后,延迟会增加,这在意料之中。我们预计 E 核的延迟会保持不变,直到达到 2 MB,但根据我们的测量,这种情况发生得更早。
从 2 MB 到 14 MB 的第三个区域对应的是 L3 高速缓存延迟,两种内核的延迟时间都大约为 12 ns。
系统中所有内核共享的 L3 高速缓存总大小为 18 MB。有趣的是,我们从 15 MB 开始看到一些意想不到的动态变化,而不是 18 MB。这很可能与某些访问在 L3 中丢失并需要从主存储器取回有关。
我没有显示图表中与内存延迟相对应的部分,该部分在我们越过 18MB 边界后开始出现。延迟开始急剧攀升,E 核为 24 MB,P 核为 64 MB。当缓冲区更大时,例如 500 MB,E 核的访问延迟为 45ns,P 核为 90ns。由于几乎没有负载进入 L3 高速缓存,因此可以测量内存延迟。
利用类似的技术,我们可以测量内存层次结构中各个部分的带宽。为了测量带宽,MLC 会执行加载请求,其结果不会被任何后续指令使用。这样,MLC 就能产生尽可能大的带宽。MLC 在每个配置的逻辑处理器上生成一个软件线程。每个线程访问的地址是独立的,线程之间不共享数据。与延迟实验一样,线程使用的缓冲区大小决定了 MLC 测量的是 L1/L2/L3 缓存带宽还是内存带宽。
  1. $ sudo ./mlc --max_bandwidth -k0-15 -Y -L -u -b18m
  2. Measuring Maximum Memory Bandwidths for the system
  3. Bandwidths are in MB/sec (1 MB/sec = 1,000,000 Bytes/sec)
  4. Using all the threads from each core if Hyper-threading is enabled
  5. Using traffic with the following read-write ratios
  6. ALL Reads : 349670.42
复制代码
这里有几个新选项。-k选项指定了用于测量的CPU内核列表。Y 选项告诉 MLC 使用 AVX2 负载,即每次 32 字节。选项 使用-u 标志时,每个线程共享同一个缓冲区,不分配自己的缓冲区。必须使用该选项来测量 L3 带宽(注意我们使用的是 18 MB 缓冲区,相当于 L3 缓存的大小)。
5.png

上图显示了使用英特尔 MLC 测得的测试系统的综合延迟和带宽数据。与共享 L3 高速缓存或主存储器相比,内核从 L1 和 L2 等低级高速缓存汲取的带宽要高得多。分析四个基准测试的性能指标 L3 和 E 核 L2 等高速缓存可以很好地同时满足多个内核的请求。例如,单个 E 核 L2 带宽为 100GB/s。在使用同一集群的两个 E 核时,我测出的带宽为 140GB/s,三个 E 核为 165GB/s,而所有四个 E 核都能从共享二级缓存中获得 175GB/s 的带宽。三级缓存也是如此,单个 P 核心的速度为 60 GB/s,而单个 E 核心的速度仅为 25 GB/s。但当所有内核都使用时,L3 高速缓存的带宽可达 300 GB/s。从内存读取数据的速度为 33.7 GB/s,而在我的平台上,理论最大带宽为 38.4 GB/s。
了解机器的主要特性是评估程序如何充分利用可用资源的基础。我们将在第 5.5 节讨论 Roofline 性能模型时再次讨论这一主题。如果您经常在单一平台上分析性能,那么最好记住内存层次结构各组成部分的延迟和带宽,或者将它们放在手边。这有助于建立被测系统的心智模型,从而有助于进一步的性能分析,接下来您将看到这一点。
4.11 案例研究:

分析四个基准的性能指标 为了将本章讨论的所有内容结合起来,我们来看一些实际案例。我们运行了来自不同领域的四个基准,并计算了它们的性能指标。首先介绍一下这些基准。
  1. Blender 3.4 - 一个开源的3D创建和建模软件项目。这个测试是使用Blender的Cycles性能进行的,使用了BMW27混合文件。使用了所有的硬件线程。URL: https://download.blender.org/release。命令行:./blender -b bmw27_cpu.blend -noaudio --enable-autoexec -o output.test -x 1 -F JPEG -f 1。
  2. Stockfish 15 - 一个先进的开源国际象棋引擎。这个测试是一个内置的stockfish基准测试。只使用了一个硬件线程。URL: https://stockfishchess.org。命令行:./stockfish bench 128 1 24 default depth。
  3. Clang 15 自我构建 - 这个测试使用clang 15从源代码构建clang 15编译器。使用了所有的硬件线程。URL: https://www.llvm.org。命令行:ninja -j16 clang。
  4. CloverLeaf 2018 - 一个拉格朗日-欧拉流体动力学基准测试。使用了所有的硬件线程。这个测试使用了clover_bm.in输入文件(问题5)。URL: http://uk-mac.github.io/CloverLeaf。命令行:./clover_leaf。
复制代码
在本练习中,我在具有以下特性的机器上运行了所有四个基准测试:

  • 第 12 代 Alder Lake Intel® Core™ i7-1260P CPU,主频 2.10GHz(4.70GHz Turbo),4P+8E 内核,18MB L3 缓存
  • 16 GB 内存,DDR4 @ 2400 MT/s - 256GB NVMe PCIe M.2 SSD
  • 64 位 Ubuntu 22.04.1 LTS (Jammy Jellyfish)
  • Clang-15 C++ 编译器,带以下选项:-O3 -march=core-avx2
为了收集性能指标,我使用了 Andi Kleen 的 pmutools:71 中的 toplev.py 脚本: $ ~/workspace/pmu-tools/toplev.py -m --global --no-desc -v --  表 4.3 提供了性能指标的并行分析。
表 4.3 对四个基准的性能指标进行了并列比较。通过观察这些指标,我们可以了解这些工作负载的性质。
6.png

以下是我们对基准性能的假设:

  • Blender。工作在 P 核和 E 核之间的分配相当平均,两种内核类型的 IPC 都不错。每千条指令的缓存未命中次数相当低(参见 LMPKI)。分支预测错误是一个小瓶颈:分支错误预测 Misp. 比率为 2%;每 610 条指令中就有 1 条错误预测(参见 IpMispredict 指标),结果相当不错。TLB 并非瓶颈,因为我们在 STLB 中很少出现缺失。我们忽略了负载缺失延迟指标,因为缓存缺失的次数非常少。ILP 相当高。
    Golden Cove 是一个 6 宽架构;3.67 的 ILP 意味着该算法每个周期几乎要使用 2/3 的内核资源。内存带宽需求较低(仅为 1.58 GB/s),远未达到这台机器的理论最大值。
    从 Ip
    指标可以看出,Blender 是一种浮点运算(参见 IpFLOP 指标),其中很大一部分是矢量化 FP 运算(参见 IpArith AVX128)。但算法的某些部分也是非矢量标量 FP 单精度指令(IpArith Scal SP)。此外,请注意每第 90 条指令都是明确的软件内存预取(IpSWPF);我们希望在 Blender 的源代码中看到这些提示。初步结论
    Blender 的性能受到 FP 计算的限制。
  • Stockfish 我们只使用一个硬件线程运行它,因此正如预期的那样,E-cores 上的工作为零。L1 miss 的数量相对较高,但其中大部分都包含在 L2 和 L3 缓存中。分支错误预测率很高;我们每 215 条指令就要支付一次错误预测惩罚。我们可以估计,每 215(指令)/1.80(IPC)=120 个周期就会发生一次错误预测,频率非常高。与 Blender 的推理类似,我们可以说 TLB 和 DRAM 带宽对 Stockfish 来说不是问题。更进一步,我们可以看到工作负载中几乎没有 FP 操作(参见 IpFLOP 指标)。
    初步结论 Stockfish 是一种整数计算工作负载,受分支错误预测的影响很大。
  • Clang 15 自构建。编译 C++ 代码是性能曲线非常平坦的任务之一,即没有大的热点。您将看到运行时间是由许多不同的函数决定的。我们首先发现,P-cores 比 E-cores 多做 68% 的工作,IPC 性能比 E-cores 高 42%。
    IPC。但 P 核和 E 核的 IPC 都很低。L*MPKI 指标乍看起来并不令人担忧,但结合负载未命中实际延迟(LdMissLat,以核心时钟为单位),我们可以发现缓存未命中的平均成本相当高(约 77 个周期)。现在,当我们查看 *STLB_MPKI 指标时,我们会发现它与我们测试的其他基准有很大的不同。这是由于 Clang 编译器(以及其他编译器)的另一个方面造成的:二进制文件的大小相对较大(超过 100 MB)。代码不断跳转到遥远的地方,给 TLB 子系统造成很大压力。正如您所看到的,指令(参见 Code stlb MPKI)和数据(参见 Ld stlb MPKI)都存在问题。让我们继续分析。DRAM 带宽使用率高于前两个基准测试,但仍未达到我们平台最大内存带宽(约 34 GB/s)的一半。
    我们关注的另一个问题是每次调用(IpCall)的指令数非常少:
    每次函数调用只有约 41 条指令。不幸的是,这是编译器代码库的性质决定的:它有成千上万个小函数。编译器需要更积极地内联所有这些函数和封装器72 。然而,我们怀疑与函数调用相关的性能开销仍然是 Clang 编译器的一个问题。此外,我们还可以发现高 ipBranch 和 IpMispredict 指标。在 Clang 编译中,通过使用链接时间优化(LTO),每五条指令中就有 72 个分支,每 35 个分支中就有一个分支。
    分支,每 35 个分支中就有一个被错误预测。几乎没有 FP 或矢量指令,但这并不奇怪。初步结论:
    Clang 拥有庞大的代码库、扁平的配置文件、许多小函数和 “多分支 ”代码。
    性能受到数据缓存未命中、TLB 未命中和分支预测错误的影响。
  • CloverLeaf 和以前一样,我们首先分析指令和内核周期。P 核和 E 核完成的工作量大致相同,但 P 核需要更多时间来完成这些工作,因此 P 核上一个逻辑线程的 IPC 要低于一个物理 E 核。73 LMPKI 指标很高,尤其是每千条指令的 L3 缺失数。负载未命中延迟 (LdMissLat) 高得离谱,表明平均高速缓存未命中价格极高。接下来,我们看看 DRAM BW 使用指标,发现内存带宽消耗已接近极限。这就是问题所在:系统中的所有内核共享相同的内存总线,因此它们会竞争访问主内存,这实际上会导致执行停滞。CPU 所需的数据供应不足。更进一步,我们可以看到 CloverLeaf 不会出现预测错误或函数调用开销过大的问题。指令组合以 FP 双精度标量运算为主,代码的某些部分被矢量化。初步结论:多线程 CloverLeaf 受内存带宽限制。
    从这项研究中可以看出,仅通过观察指标就能了解程序的许多行为。它能回答 “是什么?”的问题,但不能告诉你 “为什么?”。为此,你需要收集性能曲线,我们将在后面的章节中介绍。在本书的第 2 部分,我们将讨论如何缓解我们所分析的四个基准中存在的性能问题。
    请记住,表中的性能指标摘要只能说明程序的平均行为。例如,我们可能看到 CloverLeaf 的 IPC 为 0.2,但实际上它可能从未以这样的 IPC 运行。相反,它可能有两个持续时间相同的阶段,其中一个阶段的 IPC 为 0.1,第二个阶段的 IPC 为 0.3。性能工具通过报告每个指标的统计数据和平均值来解决这个问题。通常,有最小值、最大值、第 95 百分位数和变差(stdev/avg)就足以了解分布情况。此外,有些工具还允许绘制数据图,这样就能看到程序运行期间某个指标值的变化情况。例如,图 4.4 显示了 CloverLeaf 基准的 IPC、L
    MPKI、DRAM BW 和平均频率的动态变化。只要添加 --xlsx 和 --xchart 选项,pmu-tools 软件包就能自动生成这些图表。-I 10000 选项以 10 秒为间隔汇总所收集的样本。
  1. $ ~/workspace/pmu-tools/toplev.py -m --global --no-desc -v --xlsx workload.xlsx -xchart -I 10000 -- ./clover_leaf 尽
复制代码
管摘要中报告的平均值偏差不大,但我们可以看到工作负载并不稳定。对于 P 核心,我们可以假设工作负载没有明显的阶段,而变化是由性能事件之间的多路复用引起的(第 5.3 节将讨论)。但这只是一个假设,还需要进一步证实或推翻。
7.png

可能的方法是以更高的粒度(在我们的案例中为 10 秒)运行采集,从而收集更多的数据点。绘制 L*MPKI 的图表显示,所有三个指标都在平均值附近徘徊,没有太大偏差。DRAM 带宽利用率图表显示,主内存在不同时期承受着不同的压力。最后一张图表显示了所有 CPU 内核的平均频率。从图表中可以看出,在最初的 10 秒钟后开始出现节流现象。我建议在仅通过观察总体数据得出结论时要小心谨慎,因为这些数据可能并不能很好地反映工作负载的行为。
请记住,收集性能指标并不能代替对代码的研究。请始终尝试通过检查代码的相关部分来解释您所看到的数字。
总之,性能指标可帮助您建立正确的心智模型,了解程序中发生了什么和没有发生什么。进一步分析时,这些数据将为您提供很好的帮助。
4.12 小结


  • 在本章中,我们介绍了性能分析的基本指标,如退役/已执行指令、CPU 利用率、IPC/CPI、µops、流水线slot、内核/参考时钟、缓存未命中和分支误预测。我们展示了如何利用 Linux perf 收集这些指标。
  • 对于更高级的性能分析,您还可以收集许多衍生指标。例如,每千条指令的缓存未命中率(MPKI)、每个函数调用、分支、加载等的指令数(Ip*)、ILP、MLP 等。本章的案例研究展示了如何通过分析这些指标获得可行的见解。
  • 通过观察总体数字得出结论要谨慎。不要陷入 “Excel 性能工程 ”的陷阱,即只收集性能指标,而从不查看代码。一定要寻找第二个数据源(如性能配置文件,稍后讨论)来验证您的想法。
  • 内存带宽和延迟是当今许多生产软件包(包括人工智能、高性能计算、数据库和许多通用应用程序)性能的关键因素。内存带宽取决于 DRAM 速度(MT/s)和内存通道数量。现代高端服务器平台有 8-12 个内存通道,整个系统的带宽最高可达 500 GB/s,单线程模式下最高可达 50 GB/s。现在的内存延迟变化不大,事实上,随着新一代 DDR4 和 DDR5 的推出,延迟会略有恶化。大多数面向客户端的现代系统每次内存访问的延迟时间在 70-110 ns 之间。服务器平台的内存延迟可能更高。

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册