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

概要

llvm-exegesis [选项]

描述

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

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

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

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

支持的平台

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

代码片段注释

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

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

  • LLVM-EXEGESIS-LIVEIN <寄存器名称> - 此注释允许指定在开始基准测试时应保留其值的寄存器。在某些情况下,可以通过寄存器从基准测试设置中传递值。可以在基准测试脚本中使用 LLVM-EXEGESIS-LIVEIN 利用的寄存器及其分配的值如下所示

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

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

  • 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 文档(默认情况下写入标准输出,但您可以使用 –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 检查寄存器的活跃性(即任何寄存器使用都有相应的定义或为“活动输入”)。如果您的代码依赖于某些寄存器的值,则需要使用代码片段注释以确保正确执行设置。

例如,以下代码片段依赖于 XMM1(工具将设置)和通过 RDI 传递的内存缓冲区(活动输入)的值。

# 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

我们需要分配至少 8 个字节的内存,从 0x2000 开始。我们可以使用添加到代码片段中的以下注释创建必要的执行环境

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

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

示例 4:分析

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

  $ 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/snippet size 次)。loop 将(可选地)重复代码片段,直到循环体包含至少 loop-body-size 条指令,然后将结果包装在一个循环中,该循环将执行 min-instructions 条指令(因此,再次将代码片段重复 min-instructions/snippet size 次)。loop 模式,尤其是在循环展开的情况下,往往更好地隐藏了 CPU 前端对缓存解码指令的架构的影响,但会消耗一个寄存器来计数迭代次数。如果对许多操作码进行分析,最好使用 min 模式,该模式将运行每种其他模式,并生成最小的测量结果。中间一半重复模式将根据特定模式重复代码片段或在循环中运行代码片段。中间一半重复模式将运行两个基准测试,一个基准测试是第一个基准测试的两倍长,然后减去它们之间的差值以获得没有开销的值。

--min-instructions=<Number of instructions>

指定目标执行指令数。请注意,代码片段的实际重复次数将为 min-instructions/snippet size。较高的值会导致更准确的测量,但会延长基准测试。

--loop-body-size=<Preferred loop body size>

仅对 -repetition-mode=[loop|min] 有效。不要直接循环遍历代码片段,而是先将其重复,以便循环体包含至少这么多指令。这可能会导致循环体缓存在 CPU 操作缓存/循环缓存中,这可能会比 CPU 解码器具有更高的吞吐量。

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

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

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

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

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

如果提供,则将分析聚类作为 CSV 写入此文件。“-” 打印到标准输出。默认情况下,不会运行此分析。

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

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

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

默认情况下,会分析所有基准测试结果,但有时可能只需要查看不涉及内存的基准测试结果,反之亦然。此选项允许保留所有基准测试,或筛选出(忽略)所有涉及内存的基准测试(涉及可能读取或写入内存的指令),或相反,仅保留此类基准测试。

--analysis-clustering=[dbscan,naive]

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

--analysis-numpoints=<dbscan numPoints parameter>

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

--analysis-clustering-epsilon=<dbscan epsilon parameter>

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

--analysis-inconsistency-epsilon=<epsilon>

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

--analysis-display-unstable-clusters

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

--ignore-invalid-sched-class=false

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

--mtriple=<triple name>

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

--mcpu=<cpu name>

如果设置,则使用此 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=<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 子系统与用于测量感兴趣值的性能计数器一起分组进行测量。此标志可以多次指定以测量多个事件。验证计数器的最大数量取决于平台。

退出状态

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