llvm-exegesis - LLVM 机器指令基准测试

概要

llvm-exegesis [选项]

描述

llvm-exegesis 是一个基准测试工具,它使用 LLVM 中可用的信息来测量主机机器指令的特性,如延迟、吞吐量或端口分解。

给定一个 LLVM 操作码名称和一个基准测试模式,llvm-exegesis 生成一个代码片段,使执行尽可能串行(或并行),以便我们可以测量指令的延迟(或反向吞吐量/uop 分解)。代码片段被即时编译,除非请求不这样做,否则在主机子目标上执行。所花费的时间(或资源使用量)使用硬件性能计数器进行测量。结果以 YAML 格式打印到标准输出。

该工具的主要目标是自动(验证或否定)LLVM 的 TableDef 调度模型。为此,我们还提供结果分析。

llvm-exegesis 也可以对用户提供的任意代码片段进行基准测试。

支持的平台

llvm-exegesis 目前仅支持在 Linux 上对 X86(仅限 64 位)、ARM(仅限 AArch64,代码片段生成稀疏)、MIPS 和 PowerPC(仅限 PowerPC64LE)进行基准测试。并非所有基准测试功能都保证在每个平台上都能工作。llvm-exegesis 还具有一个单独的分析模式,该模式在 LLVM 支持的每个平台上都受支持。

要在 llvm-exegesis 中启用基准测试,LLVM 必须配置并构建时启用 LLVM_ENABLE_LIBPFM,因为 llvm-exegesis 依赖于 libpfm4 来访问性能计数器。如果目标 CPU 不受 libpfm 支持,则基准测试可能会失败。可以通过设置 LIBPFM_VERBOSELIBPFM_DEBUG 环境变量来启用 libpfm 的详细或调试模式来验证这一点。如果 libpfm 安装在非标准目录中,则可以通过设置 LIBRARY_PATHC_INCLUDE_PATHCPLUS_INCLUDE_PATH 环境变量来配置 LLVM 以定位必要的库和头文件。此外,应设置 LD_LIBRARY_PATH,以便 llvm-exegesis 在执行期间可以找到 libpfm 库。

代码片段注解

llvm-exegesis 支持对任意汇编代码片段进行基准测试。但是,对这些代码片段进行基准测试通常需要一些设置,以便它们可以正确执行。llvm-exegesis 有五个注解和一些额外的实用程序来帮助进行设置,以便可以正确地对代码片段进行基准测试。

  • LLVM-EXEGESIS-DEFREG <寄存器名称> - 将此注解添加到要进行基准测试的文本汇编代码片段,会将寄存器标记为需要定义。除非传入第二个参数,即十六进制值,否则将自动提供一个值。这是通过 LLVM-EXEGESIS-DEFREG <寄存器名称> <十六进制值> 格式完成的。<十六进制值> 是用于填充寄存器的位模式。如果它是一个小于寄存器的值,则会进行符号扩展以匹配寄存器的大小。

  • LLVM-EXEGESIS-LIVEIN <寄存器名称> - 此注解允许指定在启动基准测试时应保留其值的寄存器。在某些情况下,值可以通过寄存器从基准测试设置中传递。可以在基准测试脚本中使用 LLVM-EXEGESIS-LIVEIN 的寄存器和分配给它们的值如下:

    • 暂存内存寄存器 - 放置此值的特定寄存器取决于平台(例如,在 X86 Linux 上是 RDI 寄存器)。将此寄存器设置为 live in 可确保将指向内存块 (1MB) 的指针放置在此寄存器中,供代码片段使用。

  • LLVM-EXEGESIS-MEM-DEF <值名称> <大小> <值> - 此注解允许指定内存定义,这些定义稍后可以使用 LLVM-EXEGESIS-MEM-MAP 注解映射到代码片段的执行过程中。每个值都使用 <值名称> 参数命名,以便稍后可以在 map 注解中引用它。大小以十进制字节数指定,值以十六进制给出。如果值的大小小于指定的大小,则该值将重复,直到它填充整个内存段。使用此注解需要使用子进程执行模式。

  • LLVM-EXEGESIS-MEM-MAP <值名称> <地址> - 此注解允许将先前定义的内存定义映射到进程的执行上下文中。值名称指的是先前定义的内存定义,地址是一个十进制数字,指定内存定义应从哪个地址开始。请注意,单个内存定义可以映射多次。使用此注解需要子进程执行模式。

  • LLVM-EXEGESIS-SNIPPET-ADDRESS <地址> - 此注解允许设置要执行的代码片段的开头将映射到的地址。地址以十六进制给出。请注意,代码片段还包括设置代码,因此正好在指定地址的指令将不是代码片段中的第一条指令。使用此注解需要子进程执行模式。这在代码片段访问的内存取决于代码片段的位置的情况下很有用,例如 RIP 相对寻址。

  • LLVM-EXEGESIS-LOOP-REGISTER <寄存器名称> - 此注解指定在使用循环重复模式时用于跟踪当前迭代的循环寄存器。llvm-exegesis 需要以高性能的方式(即,没有内存访问)在循环重复模式中跟踪当前循环迭代,并使用寄存器来执行此操作。此寄存器具有特定于体系结构的默认值(例如,X86 上的 R8),但这可能与某些代码片段冲突。此注解允许更改寄存器以防止循环索引寄存器和代码片段之间的干扰。

示例 1:基准测试指令

假设你有一台 X86-64 机器。要测量单个指令的延迟,请运行

$ llvm-exegesis --mode=latency --opcode-name=ADD64rr

测量指令的 uop 分解或反向吞吐量的工作方式类似

$ llvm-exegesis --mode=uops --opcode-name=ADD64rr
$ llvm-exegesis --mode=inverse_throughput --opcode-name=ADD64rr

输出是一个 YAML 文档(默认写入 stdout,但你可以使用 –benchmarks-file 将输出重定向到文件)

---
key:
  opcode_name:     ADD64rr
  mode:            latency
  config:          ''
cpu_name:        haswell
llvm_triple:     x86_64-unknown-linux-gnu
num_repetitions: 10000
measurements:
  - { key: latency, value: 1.0058, debug_string: '' }
error:           ''
info:            'explicit self cycles, selecting one aliasing configuration.
Snippet:
ADD64rr R8, R8, R10
'
...

要测量主机体系结构的所有指令的延迟,请运行

$ llvm-exegesis --mode=latency --opcode-index=-1

示例 2:基准测试自定义代码片段

要测量自定义代码片段的延迟/uops,你可以指定 snippets-file 选项(- 从标准输入读取)。

$ echo "vzeroupper" | llvm-exegesis --mode=uops --snippets-file=-

实际的代码片段通常依赖于寄存器或内存。llvm-exegesis 检查寄存器的活跃性(即,任何寄存器使用都有相应的定义或是一个“live in”)。如果你的代码依赖于某些寄存器的值,则需要使用代码片段注解来确保正确执行设置。

例如,以下代码片段依赖于 XMM1(将由工具设置)和 RDI(live in)中传递的内存缓冲区的值。

# LLVM-EXEGESIS-LIVEIN RDI
# LLVM-EXEGESIS-DEFREG XMM1 42
vmulps        (%rdi), %xmm1, %xmm2
vhaddps       %xmm2, %xmm2, %xmm3
addq $0x10, %rdi

示例 3:使用内存注解进行基准测试

有些代码片段需要在特定位置进行内存设置才能执行而不会崩溃。可以使用 LLVM-EXEGESIS-MEM-DEFLLVM-EXEGESIS-MEM-MAP 注解来完成内存设置。要执行以下代码片段

movq $8192, %rax
movq (%rax), %rdi

我们需要至少分配八个字节的内存,从 0x2000 开始。我们可以通过将以下注解添加到代码片段来创建必要的执行环境

# LLVM-EXEGESIS-MEM-DEF test1 4096 7fffffff
# LLVM-EXEGESIS-MEM-MAP test1 8192

movq $8192, %rax
movq (%rax), %rdi

示例 4:分析

假设你有一组基准测试的指令(延迟或 uops),以 YAML 格式存储在文件 /tmp/benchmarks.yaml 中,你可以使用以下命令分析结果

  $ llvm-exegesis --mode=analysis \
--benchmarks-file=/tmp/benchmarks.yaml \
--analysis-clusters-output-file=/tmp/clusters.csv \
--analysis-inconsistencies-output-file=/tmp/inconsistencies.html

这会将指令分组到具有相同性能特征的集群中。集群将以以下格式写入到 /tmp/clusters.csv

cluster_id,opcode_name,config,sched_class
...
2,ADD32ri8_DB,,WriteALU,1.00
2,ADD32ri_DB,,WriteALU,1.01
2,ADD32rr,,WriteALU,1.01
2,ADD32rr_DB,,WriteALU,1.00
2,ADD32rr_REV,,WriteALU,1.00
2,ADD64i32,,WriteALU,1.01
2,ADD64ri32,,WriteALU,1.01
2,MOVSX64rr32,,BSWAP32r_BSWAP64r_MOVSX64rr32,1.00
2,VPADDQYrr,,VPADDBYrr_VPADDDYrr_VPADDQYrr_VPADDWYrr_VPSUBBYrr_VPSUBDYrr_VPSUBQYrr_VPSUBWYrr,1.02
2,VPSUBQYrr,,VPADDBYrr_VPADDDYrr_VPADDQYrr_VPADDWYrr_VPSUBBYrr_VPSUBDYrr_VPSUBQYrr_VPSUBWYrr,1.01
2,ADD64ri8,,WriteALU,1.00
2,SETBr,,WriteSETCC,1.01
...

llvm-exegesis 还会分析集群以指出调度信息中的不一致之处。输出是一个 html 文件。例如,/tmp/inconsistencies.html 将包含如下消息

../_images/llvm-exegesis-analysis.png

请注意,只有在调试模式下编译 llvm-exegesis 时,才会解析调度类名称,否则只会显示类 ID。但这不会使任何分析结果无效。

选项

--help

打印命令行选项的摘要。

--opcode-index=<LLVM 操作码索引>

通过索引指定要测量的操作码。指定 -1 将导致测量每个现有的操作码。有关详细信息,请参见示例 1。必须设置 opcode-indexopcode-namesnippets-file 之一。

--opcode-name=<操作码名称 1>,<操作码名称 2>,...

通过名称指定要测量的操作码。可以将多个操作码指定为逗号分隔的列表。有关详细信息,请参见示例 1。必须设置 opcode-indexopcode-namesnippets-file 之一。

--snippets-file=<文件名>

指定要测量的自定义代码片段。有关详细信息,请参见示例 2。必须设置 opcode-indexopcode-namesnippets-file 之一。

--mode=[latency|uops|inverse_throughput|analysis]

指定运行模式。请注意,某些模式有额外的要求和选项。

latency 模式可以使用 RDTSC 或 LBR。latency[LBR] 仅在 X86(至少 Skylake)上可用。要在 latency 模式下运行,必须为 x86-lbr-sample-period–repetition-mode=loop 指定正值。

analysis 模式下,你还需要指定 -analysis-clusters-output-file=-analysis-inconsistencies-output-file= 中的至少一个。

--benchmark-phase=[prepare-snippet|prepare-and-assemble-snippet|assemble-measured-code|measure]

默认情况下,当指定 -mode= 时,将执行和测量生成的代码片段,这要求我们在生成代码片段的硬件上运行,并且该硬件支持性能测量。但是,可以在测量之前的某个阶段停止。选项包括:* prepare-snippet: 仅生成最小的指令序列。* prepare-and-assemble-snippet: 与 prepare-snippet 相同,但也转储序列的摘录(十六进制编码)。* assemble-measured-code: 与 prepare-and-assemble-snippet 相同。但也创建可以使用 --dump-object-to-disk 转储到文件的完整序列。* measure: 与 assemble-measured-code 相同,但也运行测量。

--x86-lbr-sample-period=<nBranches/sample>

指定 LBR 采样周期 - 在我们进行采样之前有多少个分支。当为此选项指定正值且模式为 latency 时,我们将使用 LBR 进行测量。在选择“正确”的采样周期时,首选小值,但如果采样频率过高,可能会发生节流。应使用质数以避免持续跳过某些块。

--x86-disable-upper-sse-registers

使用较高的 xmm 寄存器 (xmm8-xmm15) 会强制使用更长的指令编码,这可能会对前端提取和解码阶段造成更大的压力,从而可能降低指令调度到后端的速度,尤其是在较旧的硬件上。将启用此模式的基准结果进行比较可以帮助确定前端的影响,并可用于改进延迟和吞吐量估计。

--repetition-mode=[duplicate|loop|min|middle-half-duplicate|middle-half-loop]

指定重复模式。duplicate 将创建一个大的、直线型的基本块,其中包含 min-instructions 条指令(重复代码片段 min-instructions/代码片段大小 次)。loop 将选择性地复制代码片段,直到循环体包含至少 loop-body-size 条指令,然后将结果包装在一个循环中,该循环将执行 min-instructions 条指令(因此,再次重复代码片段 min-instructions/代码片段大小 次)。loop 模式,特别是使用循环展开,往往可以更好地隐藏 CPU 前端对缓存解码指令的体系结构的影响,但会消耗一个寄存器来计数迭代。如果在许多操作码上执行分析,则最好改用 min 模式,该模式将运行每个其他模式,并产生最小的测量结果。中间一半重复模式将根据特定模式复制或在循环中运行代码片段。中间一半重复模式将运行两个基准测试,一个的长度是第一个的两倍,然后减去它们之间的差异以获得没有开销的值。

--min-instructions=<指令数量>

指定要执行的指令的目标数量。请注意,代码片段的实际重复计数将为 min-instructions/代码片段大小。值越高,测量结果越准确,但会延长基准测试时间。

--loop-body-size=<首选循环体大小>

仅对 -repetition-mode=[loop|min] 有效。与其直接循环代码片段,不如先复制它,以便循环体至少包含这么多指令。这可能会导致循环体缓存在 CPU Op Cache / Loop Cache 中,这可能比 CPU 解码器具有更高的吞吐量。

--max-configs-per-opcode=<值>

指定可以为每个操作码生成的最大配置数。默认情况下,这是 1,这意味着我们假设单个测量足以表征一个操作码。这可能不适用于所有指令:例如,X86 上 LEA 指令的性能特征取决于分配的寄存器和立即数的值。将 -max-configs-per-opcode 的值设置为大于 1 允许 llvm-exegesis 探索更多配置,以发现某些寄存器或立即数赋值是否会导致不同的性能特征。

--benchmarks-file=</path/to/file>

用于读取(analysis 模式)或写入(latency/uops/inverse_throughput 模式)基准测试结果的文件。“-” 使用 stdin/stdout。

--analysis-clusters-output-file=</path/to/file>

如果提供,则将分析集群以 CSV 格式写入此文件。“-” 打印到 stdout。默认情况下,不运行此分析。

--analysis-inconsistencies-output-file=</path/to/file>

如果非空,则将分析期间发现的不一致之处写入此文件。- 打印到 stdout。默认情况下,不运行此分析。

--analysis-filter=[all|reg-only|mem-only]

默认情况下,分析所有基准测试结果,但有时仅查看那些不涉及内存的结果可能很有用,反之亦然。此选项允许保留所有基准测试,或过滤掉(忽略)所有涉及内存的基准测试(涉及可能读取或写入内存的指令),或者相反,仅保留此类基准测试。

--analysis-clustering=[dbscan,naive]

指定要使用的聚类算法。默认情况下将使用 DBSCAN。朴素聚类算法更适合对 -analysis-inconsistencies-output-file= 输出进行进一步处理,它将为每个操作码创建一个集群,并检查集群是否稳定(所有点都是邻居)。

--analysis-numpoints=<dbscan numPoints 参数>

指定用于 DBSCAN 聚类的 numPoints 参数(analysis 模式,仅限 DBSCAN)。

--analysis-clustering-epsilon=<dbscan epsilon 参数>

指定用于基准测试点聚类的 epsilon 参数(analysis 模式)。

--analysis-inconsistency-epsilon=<epsilon>

指定用于检测集群何时与 LLVM 调度配置文件值不同的 epsilon 参数(analysis 模式)。

--analysis-display-unstable-clusters

如果一个操作码有多个基准测试,则如果测量的性能特征不同,这些基准测试可能最终不会聚类到同一个集群中。默认情况下,所有此类操作码都会被过滤掉。此标志将改为仅显示此类不稳定的操作码。

--ignore-invalid-sched-class=false

如果设置,则忽略没有调度类(类 idx = 0)的指令。

--mtriple=<三元组名称>

目标三元组。有关可用目标,请参见 -version

--mcpu=<cpu 名称>

如果设置,则使用此 CPU 的计数器测量 CPU 特性。这在创建新的调度模型时很有用(主机 CPU 对 LLVM 是未知的)。(-mcpu=help 获取详细信息)

--analysis-override-benchmark-triple-and-cpu

默认情况下,llvm-exegesis 将分析为其测量的三元组/CPU 的基准测试,但如果你想为其他组合(通过 -mtriple/-mcpu 指定)分析它们,你可以传递此标志。

--dump-object-to-disk=true

如果设置,llvm-exegesis 会将生成的代码转储到临时文件以启用代码检查。默认情况下禁用。

--use-dummy-perf-counters

如果设置,llvm-exegesis 将不会读取任何真实的性能计数器,而是返回一个虚拟值。这可以用于确保在硬件性能计数器不可用时代码片段不会崩溃,以及用于调试 llvm-exegesis 本身。

--execution-mode=[inprocess,subprocess]

此选项指定要使用的执行模式。inprocess 执行模式是默认模式。subprocess 执行模式允许额外的功能,例如内存注解,但目前仅限于 Linux 上的 X86-64。

--benchmark-repeat-count=<重复计数>

此选项允许指定在执行延迟测量时重复测量的次数。默认情况下,llvm-exegesis 将重复延迟测量足够多的次数,以平衡运行时间和噪声降低。

--validation-counter=[instructions-retired,l1d-cache-load-misses,
l1d-cache-store-misses,l1i-cache-load-misses,data-tlb-load-misses,
data-tld-store-misses,instruction-tlb-load-misses]

此选项启用验证计数器的使用,该计数器测量额外的微体系结构事件(如缓存未命中)以验证代码片段执行条件。这些事件是使用 perf 子系统在一个组中与用于测量感兴趣值的性能计数器一起测量的。可以多次指定此标志以测量多个事件。验证计数器的最大数量取决于平台。

--benchmark-process-cpu=<cpu id>

此选项指定应用于运行基准测试子进程的 CPU 编号。启动子进程时,llvm-exegesis 将设置子进程的亲和性,使其仅包含指定的 CPU。此选项仅在 subprocess 执行模式下有效。

退出状态

llvm-exegesis 成功时返回 0。否则,错误消息将打印到标准错误,并且该工具返回非 0 值。