现代CPU性能调校里性能剖析的术语及指标探微

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,可轻松获取退休指令的数量:

$ perf stat -e instructions -- ls
code  my.script  openpbs-23.06.06  slurm-8.out  snap  v23.06.06

 Performance counter stats for 'ls':

         1,991,109      instructions

       0.002156994 seconds time elapsed

       0.000000000 seconds user
       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利用率:

# perf stat  -- ls
code  my.script  openpbs-23.06.06  slurm-8.out  snap  v23.06.06

 Performance counter stats for 'ls':

              0.82 msec task-clock                       #    0.622 CPUs utilized
                 0      context-switches                 #    0.000 /sec
                 0      cpu-migrations                   #    0.000 /sec
                93      page-faults                      #  113.459 K/sec
         1,762,015      cycles                           #    2.150 GHz
         2,005,311      instructions                     #    1.14  insn per cycle
           371,410      branches                         #  453.116 M/sec
            11,882      branch-misses                    #    3.20% of all branches
                        TopdownL1                 #     24.4 %  tma_backend_bound
                                                  #     11.8 %  tma_bad_speculation
                                                  #     38.7 %  tma_frontend_bound
                                                  #     25.1 %  tma_retiring

       0.001318194 seconds time elapsed

       0.000000000 seconds user
       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:

# perf stat -e cycles,instructions -- ls
code  my.script  openpbs-23.06.06  slurm-8.out  snap  v23.06.06

 Performance counter stats for 'ls':

         1,895,107      cycles
         1,980,477      instructions                     #    1.05  insn per cycle

       0.000726477 seconds time elapsed

       0.000766000 seconds user
       0.000000000 seconds sys
# 或 # 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在解码后被“破解”为两个从属的微操作:

SUB rsp, 8
STORE [rsp], rbx

通常,函数序幕会通过使用多条PUSH指令来保存多个寄存器。在本例中,下一条PUSH指令可在上一条PUSH指令的SUB µop结束后开始执行,无需等待STORE µop,因为STORE µop现在可异步执行。

  • 并行:以HADDPD xmm1, xmm2指令为例,该指令将xmm1和xmm2中的两个双精度浮点数值相加(还原),并将两个结果存储到xmm1中,如下所示:
xmm1[63:0] = xmm2[127:64] + xmm2[63:0]
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 。例如

.loop:
  dec rdi
  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,如下所示:

# perf stat -e uops_issued.any,uops_executed.thread,uops_retired.slots -- ls
code  my.script  openpbs-23.06.06  slurm-8.out  snap  v23.06.06

 Performance counter stats for 'ls':

         2,948,879      uops_issued.any
         2,667,937      uops_executed.thread
         2,218,621      uops_retired.slots

       0.000702776 seconds time elapsed

       0.000000000 seconds user
       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%。

英特尔的Skylake和AMD Zen3内核采用4宽分配。英特尔的Sunny Cove微架构是5宽设计。截至2023年底,最新的Golden Cove和Zen4架构均采用6宽分配。苹果M1和M2设计为8宽,苹果M3为9-µop执行带宽,见[苹果

相关文章

暂无评论

暂无评论...