llvm-mca - LLVM 机器代码分析器

概要

llvm-mca [选项] [输入]

描述

llvm-mca 是一个性能分析工具,它使用 LLVM 中可用的信息(例如调度模型)来静态地衡量特定 CPU 中机器代码的性能。

性能的衡量标准是吞吐量以及处理器资源消耗。该工具目前适用于具有后端的处理器,LLVM 中提供了这些处理器的调度模型。

该工具的主要目标不仅是预测代码在目标上运行时的性能,还有助于诊断潜在的性能问题。

给定一个汇编代码序列,llvm-mca 估计每周期指令数 (IPC),以及硬件资源压力。分析和报告风格受到了 Intel IACA 工具的启发。

例如,您可以使用 clang 编译代码,输出汇编代码,并将其直接管道输入到 llvm-mca 进行分析

$ clang foo.c -O2 --target=x86_64 -S -o - | llvm-mca -mcpu=btver2

或者对于 Intel 语法

$ clang foo.c -O2 --target=x86_64 -masm=intel -S -o - | llvm-mca -mcpu=btver2

llvm-mca 通过输入开头是否存在 .intel_syntax 指令来检测 Intel 语法。默认情况下,其输出语法与其输入语法匹配。)

调度模型不仅用于计算指令延迟和吞吐量,还用于了解可用的处理器资源以及如何模拟它们。

根据设计,llvm-mca 执行的分析质量不可避免地会受到 LLVM 中调度模型质量的影响。

如果您发现性能报告对于某个处理器不准确,请提交 bug 给相应的后端。

选项

如果 input 是 “-” 或省略,则 llvm-mca 从标准输入读取。否则,它将从指定的文件名读取。

如果省略了 -o 选项,则如果输入来自标准输入,llvm-mca 将将其输出发送到标准输出。如果 -o 选项指定 “-”,则输出也将发送到标准输出。

-help

打印命令行选项的摘要。

-o <filename>

使用 <filename> 作为输出文件名。有关更多详细信息,请参见上面的摘要。

-mtriple=<target triple>

指定目标三元组字符串。

-march=<arch>

指定要分析代码的架构。它默认为主机默认目标。

-mcpu=<cpuname>

指定要分析代码的处理器。默认情况下,CPU 名称是从主机自动检测的。

-output-asm-variant=<variant id>

指定工具生成的报告的输出汇编变体。在 x86 上,可能的值为 [0, 1]。此标志的值为 0(即 1)会为分析报告中工具打印的代码启用 AT&T(即 Intel)汇编格式。

-print-imm-hex

在作为报告一部分打印的输出汇编代码中,首选十六进制格式的数字字面量。

-dispatch=<width>

为处理器指定不同的分派宽度。分派宽度默认为处理器调度模型中的字段 ‘IssueWidth’。如果宽度为零,则使用默认分派宽度。

-register-file-size=<size>

指定寄存器文件的大小。指定后,此标志限制了可用于寄存器重命名的物理寄存器的数量。此标志的值为零表示“物理寄存器数量不受限制”。

-iterations=<number of iterations>

指定要运行的迭代次数。如果此标志设置为 0,则该工具会将迭代次数设置为默认值(即 100)。

-noalias=<bool>

如果设置,该工具会假定加载和存储不别名。这是默认行为。

-lqueue=<load queue size>

指定工具模拟的加载/存储单元中加载队列的大小。默认情况下,该工具假定加载队列中的条目数量不受限制。此标志的值为零将被忽略,而是使用默认的加载队列大小。

-squeue=<store queue size>

指定工具模拟的加载/存储单元中存储队列的大小。默认情况下,该工具假定存储队列中的条目数量不受限制。此标志的值为零将被忽略,而是使用默认的存储队列大小。

-timeline

启用时间线视图。

-timeline-max-iterations=<iterations>

限制在时间线视图中打印的迭代次数。默认情况下,时间线视图最多打印 10 次迭代的信息。

-timeline-max-cycles=<cycles>

限制时间线视图中的周期数,或使用 0 表示无限制。默认情况下,周期数设置为 80。

-resource-pressure

启用资源压力视图。默认情况下启用此功能。

-register-file-stats

启用寄存器文件使用统计信息。

-dispatch-stats

启用额外的分派统计信息。此视图收集和分析指令分派事件,以及静态/动态分派停顿事件。默认情况下禁用此视图。

-scheduler-stats

启用额外的调度器统计信息。此视图收集和分析指令发布事件。默认情况下禁用此视图。

-retire-stats

启用额外的退役控制单元统计信息。默认情况下禁用此视图。

-instruction-info

启用指令信息视图。默认情况下启用此功能。

-show-encoding

在指令信息视图中启用指令编码的打印。

-show-barriers

在指令信息视图中启用 LoadBarrier 和 StoreBarrier 标志的打印。

-all-stats

打印所有硬件统计信息。这将启用与分派逻辑、硬件调度器、寄存器文件和退役控制单元相关的额外统计信息。默认情况下禁用此选项。

-all-views

启用所有视图。

-instruction-tables

根据处理器模型中可用的静态信息打印资源压力信息。这与资源压力视图不同,因为它不需要模拟代码。相反,它打印序列中每条指令的理论均匀分布的资源压力。

-bottleneck-analysis

打印有关影响吞吐量的瓶颈的信息。此分析可能很耗时,默认情况下处于禁用状态。瓶颈在摘要视图中突出显示。目前不支持对具有按序后端的处理器进行瓶颈分析。

-json

以有效的 JSON 格式打印请求的视图。指令和处理器资源作为特殊顶级 JSON 对象的成员打印。各个视图通过索引引用它们。但是,并非当前支持所有视图。例如,瓶颈分析的报告不会以 JSON 格式打印出来。目前支持所有默认视图。

-disable-cb

强制使用通用的 CustomBehaviour 和 InstrPostProcess 类,而不是使用特定于目标的实现。通用类永远不会检测到任何自定义危害或对指令进行任何后处理修改。

-disable-im

强制使用通用的 InstrumentManager,而不是使用特定于目标的实现。通用类创建不提供额外信息的 Instrument,并且 InstrumentManager 永远不会覆盖给定指令的默认调度类。

-skip-unsupported-instructions=<reason>

强制 llvm-mca 在存在无法解析或缺少关键调度信息的指令的情况下继续运行。请注意,由于那些不受支持的指令被忽略,结果分析会受到影响,就好像它们不是输入的一部分一样。

<reason> 的选择控制 mca 何时报告错误。<reason> 可以是 none(默认)、lack-schedparse-failureany

退出状态

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

使用标记分析特定代码块

llvm-mca 允许可选地使用特殊代码注释来标记要分析的汇编代码区域。以子字符串 LLVM-MCA-BEGIN 开头的注释标记分析区域的开始。以子字符串 LLVM-MCA-END 开头的注释标记区域的结束。例如

# LLVM-MCA-BEGIN
  ...
# LLVM-MCA-END

如果未指定用户定义的区域,则 llvm-mca 假定一个默认区域,其中包含输入文件中的每条指令。每个区域都单独分析,最终性能报告是为每个分析区域生成的所有报告的并集。

分析区域可以有名称。例如

# LLVM-MCA-BEGIN A simple example
  add %eax, %eax
# LLVM-MCA-END

上面的示例中的代码定义了一个名为 “A simple example” 的区域,其中包含一条指令。请注意,区域名称不必在 LLVM-MCA-END 指令中重复。在没有重叠区域的情况下,匿名的 LLVM-MCA-END 指令始终结束当前活动的、用户定义的区域。

嵌套区域的示例

# LLVM-MCA-BEGIN foo
  add %eax, %edx
# LLVM-MCA-BEGIN bar
  sub %eax, %edx
# LLVM-MCA-END bar
# LLVM-MCA-END foo

重叠区域的示例

# LLVM-MCA-BEGIN foo
  add %eax, %edx
# LLVM-MCA-BEGIN bar
  sub %eax, %edx
# LLVM-MCA-END foo
  add %eax, %edx
# LLVM-MCA-END bar

请注意,多个匿名区域不能重叠。此外,重叠区域不能具有相同的名称。

不支持从高级源代码(如 C 或 C++)标记区域。作为一种解决方法,可以使用内联汇编指令

int foo(int a, int b) {
  __asm volatile("# LLVM-MCA-BEGIN foo":::"memory");
  a += 42;
  __asm volatile("# LLVM-MCA-END":::"memory");
  a *= b;
  return a;
}

但是,这会干扰循环向量化等优化,并可能对生成的代码产生影响。这是因为 __asm 语句被视为具有重要副作用的真实代码,这限制了可以对其周围的代码进行转换的方式。如果用户想要使用内联汇编来发出标记,那么建议始终验证输出汇编代码是否等同于在没有标记的情况下生成的汇编代码。Clang 选项以发出优化报告 也可以帮助检测遗漏的优化。

仪表区域

InstrumentRegion 描述了由特殊的 LLVM-MCA 注释指令保护的汇编代码区域。

# LLVM-MCA-<INSTRUMENT_TYPE> <data>
  ...  ## asm

其中 INSTRUMENT_TYPE 是目标定义的类型,并期望使用 data

以子字符串 LLVM-MCA-<INSTRUMENT_TYPE> 开头的注释将数据带入作用域,供 llvm-mca 在其对所有后续指令的分析中使用。

如果在指令列表中稍后找到具有相同 INSTRUMENT_TYPE 的注释,则原始 InstrumentRegion 将自动结束,并且新的 InstrumentRegion 将开始。

如果存在包含不同 INSTRUMENT_TYPE 的注释,则两个数据集都保持可用。与 AnalysisRegion 相比,InstrumentRegion 不需要注释来结束区域。

LLVM-MCA- 为前缀但不对应于目标的有效 INSTRUMENT_TYPE 的注释会导致错误,除了 BEGINEND,因为它们对应于 AnalysisRegion。不以 LLVM-MCA- 开头的注释会被 llvm-mca 忽略。

仅当指令 (MCInst) 的位置在范围 [R.RangeStart, R.RangeEnd] 内时,才将其添加到 InstrumentRegion R。

在 RISCV 目标上,向量指令具有不同的行为,具体取决于 LMUL。可以使用以下形式的注释来检测代码

# LLVM-MCA-RISCV-LMUL <M1|M2|M4|M8|MF2|MF4|MF8>

RISCV InstrumentManager 将覆盖向量指令的调度类,以使用其伪指令的调度行为,该行为取决于 LMUL。将 RISCV 仪表注释直接放在 vset{i}vl{i} 指令之后是有意义的,尽管它们可以放置在程序中的任何位置。

没有调用 vset{i}vl{i} 的程序示例

# LLVM-MCA-RISCV-LMUL M2
vadd.vv v2, v2, v2

调用 vset{i}vl{i} 的程序示例

vsetvli zero, a0, e8, m1, tu, mu
# LLVM-MCA-RISCV-LMUL M1
vadd.vv v2, v2, v2

多次调用 vset{i}vl{i} 的程序示例

vsetvli zero, a0, e8, m1, tu, mu
# LLVM-MCA-RISCV-LMUL M1
vadd.vv v2, v2, v2
vsetvli zero, a0, e8, m8, tu, mu
# LLVM-MCA-RISCV-LMUL M8
vadd.vv v2, v2, v2

调用 vsetvl 的程序示例

vsetvl rd, rs1, rs2
# LLVM-MCA-RISCV-LMUL M1
vadd.vv v12, v12, v12
vsetvl rd, rs1, rs2
# LLVM-MCA-RISCV-LMUL M4
vadd.vv v12, v12, v12

llvm-mca 的工作原理

llvm-mca 接受汇编代码作为输入。借助现有的 LLVM 目标汇编解析器,汇编代码被解析为 MCInst 序列。Pipeline 模块然后分析解析后的 MCInst 序列,以生成性能报告。

Pipeline 模块模拟机器代码序列在迭代循环中的执行(默认为 100 次)。在此过程中,Pipeline 收集了许多与执行相关的统计信息。在此过程结束时,Pipeline 从收集的统计信息生成并打印报告。

这是一个由该工具生成的性能报告示例,用于两个包含四个元素的打包浮点向量的点积。分析是针对目标 x86,cpu btver2 进行的。可以使用位于 test/tools/llvm-mca/X86/BtVer2/dot-product.s 的示例,通过以下命令生成以下结果

$ llvm-mca -mtriple=x86_64-unknown-unknown -mcpu=btver2 -iterations=300 dot-product.s
Iterations:        300
Instructions:      900
Total Cycles:      610
Total uOps:        900

Dispatch Width:    2
uOps Per Cycle:    1.48
IPC:               1.48
Block RThroughput: 2.0


Instruction Info:
[1]: #uOps
[2]: Latency
[3]: RThroughput
[4]: MayLoad
[5]: MayStore
[6]: HasSideEffects (U)

[1]    [2]    [3]    [4]    [5]    [6]    Instructions:
 1      2     1.00                        vmulps      %xmm0, %xmm1, %xmm2
 1      3     1.00                        vhaddps     %xmm2, %xmm2, %xmm3
 1      3     1.00                        vhaddps     %xmm3, %xmm3, %xmm4


Resources:
[0]   - JALU0
[1]   - JALU1
[2]   - JDiv
[3]   - JFPA
[4]   - JFPM
[5]   - JFPU0
[6]   - JFPU1
[7]   - JLAGU
[8]   - JMul
[9]   - JSAGU
[10]  - JSTC
[11]  - JVALU0
[12]  - JVALU1
[13]  - JVIMUL


Resource pressure per iteration:
[0]    [1]    [2]    [3]    [4]    [5]    [6]    [7]    [8]    [9]    [10]   [11]   [12]   [13]
 -      -      -     2.00   1.00   2.00   1.00    -      -      -      -      -      -      -

Resource pressure by instruction:
[0]    [1]    [2]    [3]    [4]    [5]    [6]    [7]    [8]    [9]    [10]   [11]   [12]   [13]   Instructions:
 -      -      -      -     1.00    -     1.00    -      -      -      -      -      -      -     vmulps      %xmm0, %xmm1, %xmm2
 -      -      -     1.00    -     1.00    -      -      -      -      -      -      -      -     vhaddps     %xmm2, %xmm2, %xmm3
 -      -      -     1.00    -     1.00    -      -      -      -      -      -      -      -     vhaddps     %xmm3, %xmm3, %xmm4

根据此报告,点积内核已执行 300 次,总共模拟了 900 条指令。模拟的微操作码 (uOp) 总数也为 900。

该报告分为三个主要部分。第一部分收集了一些性能数字;本节的目标是快速概述性能吞吐量。重要的性能指标是 IPC每周期 uOp 数块倒数吞吐量(Block Reciprocal Throughput)。

DispatchWidth 字段是每个模拟周期分派到乱序后端的最大微操作码数。对于具有按序后端的处理器,DispatchWidth 是每个模拟周期发布到后端的最大微操作码数。

IPC 的计算方法是将模拟指令总数除以周期总数。

块倒数吞吐量 字段是块吞吐量的倒数。块吞吐量是一个理论量,计算为在没有循环携带依赖关系的情况下,每个模拟时钟周期可以执行的最大块数(即迭代次数)。块吞吐量受到分派率和硬件资源可用性的上限限制。

在没有循环携带数据依赖关系的情况下,观察到的 IPC 趋向于理论最大值,该理论最大值可以通过将单次迭代的指令数除以 块倒数吞吐量 来计算。

“每周期 uOp 数”字段的计算方法是将模拟微操作码总数除以周期总数。Dispatch Width 和此字段之间的差值是指示性能问题的指标。在没有循环携带数据依赖关系的情况下,观察到的“每周期 uOp 数”应趋向于理论最大吞吐量,该理论最大吞吐量可以通过将单次迭代的 uOp 数除以 块倒数吞吐量 来计算。

每周期 uOp 数 字段的上限是分派宽度。这是因为分派宽度限制了分派组的最大大小。IPC 和“每周期 uOp 数”都受到硬件并行量的限制。硬件资源的可用性会影响资源压力分布,并且限制了每个周期可以并行执行的指令数。Dispatch Width 与理论最大 uOp 吞吐量(通过将单次迭代的 uOp 数除以 块倒数吞吐量 计算得出)之间的差值是指示硬件资源不足导致的性能瓶颈的指标。一般来说,块倒数吞吐量越低越好。

在此示例中,每次迭代的 uOp 数/块倒数吞吐量 为 1.50。由于没有循环携带依赖关系,因此当迭代次数趋于无穷大时,观察到的 每周期 uOp 数 预计会接近 1.50。分派宽度 (2.00) 与理论最大 uOp 吞吐量 (1.50) 之间的差值是指示硬件资源不足导致的性能瓶颈的指标,而资源压力视图可以帮助识别有问题的资源使用情况。

报告的第二部分是 指令信息视图。它显示序列中每条指令的延迟和倒数吞吐量。它还报告与微操作码数量和操作码属性(即“MayLoad”、“MayStore”和“HasSideEffects”)相关的额外信息。

倒数吞吐量 字段是指令吞吐量的倒数。吞吐量的计算方法是在没有操作数依赖关系的情况下,每个时钟周期可以执行的相同类型指令的最大数量。在此示例中,向量浮点乘法的倒数吞吐量为 1 周期/指令。这是因为 FP 乘法器 JFPM 仅可从流水线 JFPU1 获得。

当指定标志 -show-encoding 时,指令编码将显示在指令信息视图中。

下面是点积内核的 -show-encoding 输出示例

Instruction Info:
[1]: #uOps
[2]: Latency
[3]: RThroughput
[4]: MayLoad
[5]: MayStore
[6]: HasSideEffects (U)
[7]: Encoding Size

[1]    [2]    [3]    [4]    [5]    [6]    [7]    Encodings:                    Instructions:
 1      2     1.00                         4     c5 f0 59 d0                   vmulps %xmm0, %xmm1, %xmm2
 1      4     1.00                         4     c5 eb 7c da                   vhaddps        %xmm2, %xmm2, %xmm3
 1      4     1.00                         4     c5 e3 7c e3                   vhaddps        %xmm3, %xmm3, %xmm4

“编码大小”列显示指令的大小(以字节为单位)。“编码”列显示实际的指令编码(十六进制字节序列)。

第三部分是资源压力视图。此视图报告每次迭代中指令为目标上可用的每个处理器资源单元消耗的平均资源周期数。信息结构化为两个表。第一个表报告每次迭代平均花费的资源周期数。第二个表将资源周期与序列中的机器指令相关联。例如,vmulps 指令的每次迭代始终在资源单元 [6](JFPU1 - 浮点流水线 #1)上执行,每次迭代平均消耗 1 个资源周期。请注意,在 AMD Jaguar 上,向量浮点乘法只能发布到流水线 JFPU1,而水平浮点加法只能发布到流水线 JFPU0。

资源压力视图有助于识别由特定硬件资源的高使用率引起的瓶颈。通常应避免资源压力主要集中在少数资源上的情况。理想情况下,压力应在多个资源之间均匀分布。

时间线视图

时间线视图生成指令管道中每条指令状态转换的详细报告。此视图通过命令行选项 -timeline 启用。当指令在管道的各个阶段之间转换时,它们的状态会在视图报告中显示。这些状态由以下字符表示

  • D : 指令已分派。

  • e : 指令正在执行。

  • E : 指令已执行。

  • R : 指令已退役。

  • = : 指令已分派,等待执行。

  • - : 指令已执行,等待退役。

下面是位于 test/tools/llvm-mca/X86/BtVer2/dot-product.s 并且由 llvm-mca 使用以下命令处理的点积示例子集的时间线视图

$ llvm-mca -mtriple=x86_64-unknown-unknown -mcpu=btver2 -iterations=3 -timeline dot-product.s
Timeline view:
                    012345
Index     0123456789

[0,0]     DeeER.    .    .   vmulps   %xmm0, %xmm1, %xmm2
[0,1]     D==eeeER  .    .   vhaddps  %xmm2, %xmm2, %xmm3
[0,2]     .D====eeeER    .   vhaddps  %xmm3, %xmm3, %xmm4
[1,0]     .DeeE-----R    .   vmulps   %xmm0, %xmm1, %xmm2
[1,1]     . D=eeeE---R   .   vhaddps  %xmm2, %xmm2, %xmm3
[1,2]     . D====eeeER   .   vhaddps  %xmm3, %xmm3, %xmm4
[2,0]     .  DeeE-----R  .   vmulps   %xmm0, %xmm1, %xmm2
[2,1]     .  D====eeeER  .   vhaddps  %xmm2, %xmm2, %xmm3
[2,2]     .   D======eeeER   vhaddps  %xmm3, %xmm3, %xmm4


Average Wait times (based on the timeline view):
[0]: Executions
[1]: Average time spent waiting in a scheduler's queue
[2]: Average time spent waiting in a scheduler's queue while ready
[3]: Average time elapsed from WB until retire stage

      [0]    [1]    [2]    [3]
0.     3     1.0    1.0    3.3       vmulps   %xmm0, %xmm1, %xmm2
1.     3     3.3    0.7    1.0       vhaddps  %xmm2, %xmm2, %xmm3
2.     3     5.7    0.0    0.0       vhaddps  %xmm3, %xmm3, %xmm4
       3     3.3    0.5    1.4       <total>

时间线视图很有趣,因为它显示了指令在执行期间的状态变化。它还让您了解该工具如何处理在目标上执行的指令,以及如何计算其时序信息。

时间线视图结构化为两个表。第一个表显示指令随时间(以周期为单位衡量)变化的状态;第二个表(名为平均等待时间)报告有用的时序统计信息,这些信息应有助于诊断由长数据依赖关系和硬件资源次优使用引起的性能瓶颈。

时间线视图中的指令由一对索引标识,其中第一个索引标识迭代,第二个索引是指令索引(即,它在代码序列中出现的位置)。由于此示例是使用 3 次迭代生成的:-iterations=3,因此迭代索引的范围为 0-2(包括 0 和 2)。

除去第一列和最后一列,其余列均以周期为单位。周期从 0 开始按顺序编号。

从上面的示例输出中,我们知道以下内容

  • 指令 [1,0] 在周期 1 分派。

  • 指令 [1,0] 在周期 2 开始执行。

  • 指令 [1,0] 在周期 4 到达写回阶段。

  • 指令 [1,0] 在周期 10 退役。

指令 [1,0](即,来自迭代 #1 的 vmulps)不必在调度器的队列中等待操作数变为可用。当 vmulps 分派时,操作数已可用,并且流水线 JFPU1 已准备好服务另一条指令。因此,该指令可以立即在 JFPU1 流水线上发布。指令仅在调度器的队列中花费了 1 个周期就证明了这一点。

在写回阶段和退役事件之间存在 5 个周期的间隔。这是因为指令必须按程序顺序退役,因此 [1,0] 必须等待 [0,2] 先退役(即,它必须等到周期 10)。

在该示例中,所有指令都处于 RAW(写后读)依赖链中。vmulps 写入的寄存器 %xmm2 立即被第一个 vhaddps 使用,而第一个 vhaddps 写入的寄存器 %xmm3 被第二个 vhaddps 使用。长数据依赖关系会对 ILP(指令级并行性)产生负面影响。

在点积示例中,不同迭代的指令引入了反依赖关系。但是,可以在寄存器重命名阶段消除这些依赖关系(以分配寄存器别名,从而消耗物理寄存器为代价)。

平均等待时间表有助于诊断由长延迟指令和潜在的长数据依赖关系引起的性能问题,这些依赖关系可能会限制 ILP。最后一行 <total> 显示了对所有测量的指令的全局平均值。请注意,默认情况下,llvm-mca 假定分派事件和发布事件之间至少有 1 个周期。

当性能受到数据依赖关系和/或长延迟指令的限制时,与在调度器队列中花费的总周期数相比,在就绪状态下花费的周期数预计会非常小。两个计数器之间的差异很好地指示了数据依赖关系对指令执行的影响有多大。当性能主要受到硬件资源不足的限制时,两个计数器之间的差值很小。但是,队列中花费的周期数往往更大(即,超过 1-3 个周期),特别是与其他低延迟指令相比时。

瓶颈分析

-bottleneck-analysis 命令行选项启用性能瓶颈分析。

此分析可能很耗时。它尝试将后端压力(由流水线资源压力和数据依赖关系引起)的增加与动态分派停顿相关联。

下面是由 llvm-mca 为 btver2 上点积示例的 500 次迭代生成的 -bottleneck-analysis 输出示例。

Cycles with backend pressure increase [ 48.07% ]
Throughput Bottlenecks:
  Resource Pressure       [ 47.77% ]
  - JFPA  [ 47.77% ]
  - JFPU0  [ 47.77% ]
  Data Dependencies:      [ 0.30% ]
  - Register Dependencies [ 0.30% ]
  - Memory Dependencies   [ 0.00% ]

Critical sequence based on the simulation:

              Instruction                         Dependency Information
 +----< 2.    vhaddps %xmm3, %xmm3, %xmm4
 |
 |    < loop carried >
 |
 |      0.    vmulps  %xmm0, %xmm1, %xmm2
 +----> 1.    vhaddps %xmm2, %xmm2, %xmm3         ## RESOURCE interference:  JFPA [ probability: 74% ]
 +----> 2.    vhaddps %xmm3, %xmm3, %xmm4         ## REGISTER dependency:  %xmm3
 |
 |    < loop carried >
 |
 +----> 1.    vhaddps %xmm2, %xmm2, %xmm3         ## RESOURCE interference:  JFPA [ probability: 74% ]

根据分析,吞吐量受到资源压力的限制,而不是数据依赖关系的限制。分析观察到在 48.07% 的模拟运行期间后端压力增加。几乎所有这些压力增加事件都是由处理器资源 JFPA/JFPU0 上的争用引起的。

关键序列是根据模拟最昂贵的指令序列。它被注释以提供有关指令之间关键寄存器依赖关系和资源干扰的额外信息。

关键序列中的指令预计会对性能产生重大影响。通过构造,此分析的准确性在很大程度上取决于模拟以及(一如既往)llvm 中处理器模型的质量。

目前不支持对具有按序后端的处理器进行瓶颈分析。

用于进一步诊断性能问题的额外统计信息

-all-stats 命令行选项为分派逻辑、重排序缓冲区、退役控制单元和寄存器文件启用额外的统计信息和性能计数器。

下面是由 llvm-mca 为前面部分讨论的点积示例的 300 次迭代生成的 -all-stats 输出示例。

Dynamic Dispatch Stall Cycles:
RAT     - Register unavailable:                      0
RCU     - Retire tokens unavailable:                 0
SCHEDQ  - Scheduler full:                            272  (44.6%)
LQ      - Load queue full:                           0
SQ      - Store queue full:                          0
GROUP   - Static restrictions on the dispatch group: 0


Dispatch Logic - number of cycles where we saw N micro opcodes dispatched:
[# dispatched], [# cycles]
 0,              24  (3.9%)
 1,              272  (44.6%)
 2,              314  (51.5%)


Schedulers - number of cycles where we saw N micro opcodes issued:
[# issued], [# cycles]
 0,          7  (1.1%)
 1,          306  (50.2%)
 2,          297  (48.7%)

Scheduler's queue usage:
[1] Resource name.
[2] Average number of used buffer entries.
[3] Maximum number of used buffer entries.
[4] Total number of buffer entries.

 [1]            [2]        [3]        [4]
JALU01           0          0          20
JFPU01           17         18         18
JLSAGU           0          0          12


Retire Control Unit - number of cycles where we saw N instructions retired:
[# retired], [# cycles]
 0,           109  (17.9%)
 1,           102  (16.7%)
 2,           399  (65.4%)

Total ROB Entries:                64
Max Used ROB Entries:             35  ( 54.7% )
Average Used ROB Entries per cy:  32  ( 50.0% )


Register File statistics:
Total number of mappings created:    900
Max number of mappings used:         35

*  Register File #1 -- JFpuPRF:
   Number of physical registers:     72
   Total number of mappings created: 900
   Max number of mappings used:      35

*  Register File #2 -- JIntegerPRF:
   Number of physical registers:     64
   Total number of mappings created: 0
   Max number of mappings used:      0

如果我们查看动态分派停顿周期表,我们看到 SCHEDQ 的计数器报告了 272 个周期。每次分派逻辑由于调度器的队列已满而无法分派完整组时,此计数器都会递增。

查看分派逻辑表,我们看到管道仅在 51.5% 的时间内能够分派两个微操作码。在 44.6% 的周期中,分派组被限制为一个微操作码,这对应于 272 个周期。分派统计信息可以通过使用命令选项 -all-stats-dispatch-stats 来显示。

下一个表调度器显示了一个直方图,显示了一个计数,表示在某些周期数上发布的微操作码数。在本例中,在 610 个模拟周期中,单操作码发布了 306 次 (50.2%),并且有 7 个周期没有发布任何操作码。

调度器队列使用情况表显示了运行时使用的缓冲区条目(即,调度器队列条目)的平均值和最大值。资源 JFPU01 达到了其最大值(18 个队列条目中的 18 个)。请注意,AMD Jaguar 实现了三个调度器

  • JALU01 - ALU 指令调度器。

  • JFPU01 - 浮点运算调度器。

  • JLSAGU - 地址生成调度器。

点积是包含三个浮点指令(一个向量乘法后跟两个水平加法)的内核。 这解释了为什么只有浮点调度器看起来被使用了。

完整的调度器队列可能是由数据依赖链或硬件资源次优使用造成的。 有时,资源压力可以通过使用消耗不同调度器资源的不同指令重写内核来缓解。 队列较小的调度器对长数据依赖性造成的瓶颈的弹性较差。 调度器统计信息通过使用命令选项 -all-stats-scheduler-stats 显示。

下一个表格“Retire Control Unit”(退役控制单元)展示了一个直方图,其中显示了在一定数量的周期内退役的指令计数。 在本例中,在 610 个模拟周期中,有 399 次(65.4%)在同一周期内退役了两个指令,并且有 109 个周期没有指令退役。 退役统计信息通过使用命令选项 -all-stats-retire-stats 显示。

最后一个展示的表格是“Register File statistics”(寄存器文件统计信息)。 管道使用的每个物理寄存器文件 (PRF) 都在此表中展示。 在 AMD Jaguar 的情况下,有两个寄存器文件,一个用于浮点寄存器 (JFpuPRF),另一个用于整数寄存器 (JIntegerPRF)。 该表显示,在处理的 900 条指令中,创建了 900 个映射。 由于此点积示例仅使用了浮点寄存器,因此 JFPuPRF 负责创建 900 个映射。 但是,我们看到管道在任何给定时间最多只使用了 72 个可用寄存器槽中的 35 个。 我们可以得出结论,浮点 PRF 是该示例唯一使用的寄存器文件,并且它从未受到资源限制。 寄存器文件统计信息通过使用命令选项 -all-stats-register-file-stats 显示。

在本例中,我们可以得出结论,IPC 主要受数据依赖性限制,而不是受资源压力限制。

指令流

本节描述了 llvm-mca 的默认管道中的指令流,以及过程中涉及的功能单元。

默认管道实现了以下用于处理指令的阶段序列。

  • 分发(指令被分发到调度器)。

  • 发射(指令被发射到处理器管道)。

  • 写回(指令被执行,结果被写回)。

  • 退役(指令被退役;写入在架构上被提交)。

按序管道实现了以下阶段序列

  • 按序发射(指令被发射到处理器管道)。

  • 退役(指令被退役;写入在架构上被提交)。

llvm-mca 假定指令在模拟开始之前都已被解码并放入队列中。 因此,指令提取和解码阶段未被建模。 前端的性能瓶颈未被诊断。 此外,llvm-mca 不对分支预测进行建模。

指令分发

在分发阶段,指令按照程序顺序从已解码指令的队列中选取,并成组分发到模拟硬件调度器。

分发组的大小取决于模拟硬件资源的可用性。 处理器分发宽度默认为 LLVM 调度模型中 IssueWidth 的值。

如果满足以下条件,则可以分发指令:

  • 分发组的大小小于处理器的分发宽度。

  • 重排序缓冲区中有足够的条目。

  • 有足够的物理寄存器来进行寄存器重命名。

  • 调度器未满。

调度模型可以选择性地指定处理器上可用的寄存器文件。 llvm-mca 使用该信息来初始化寄存器文件描述符。 用户可以使用命令选项 -register-file-size 限制全局可用于寄存器重命名的物理寄存器数量。 此选项的值为零表示无限制。 通过了解可用于重命名的寄存器数量,该工具可以预测因缺少物理寄存器而导致的分发停顿。

指令消耗的重排序缓冲区条目数取决于目标调度模型为该指令指定的微操作码数量。 重排序缓冲区负责跟踪“正在进行中”的指令的进度,并按程序顺序退役它们。 重排序缓冲区中的条目数默认为目标调度模型中 MicroOpBufferSize 字段指定的值。

分发到调度器的指令会消耗调度器缓冲区条目。 llvm-mca 查询调度模型以确定指令消耗的缓冲资源集。 缓冲资源被视为调度器资源。

指令发射

每个处理器调度器都实现了一个指令缓冲区。 指令必须在调度器的缓冲区中等待,直到输入寄存器操作数变为可用。 只有到那时,指令才变得符合执行条件,并且可以(可能乱序地)发射以执行。 指令延迟由 llvm-mca 在调度模型的帮助下计算。

llvm-mca 的调度器旨在模拟多个处理器调度器。 调度器负责跟踪数据依赖性,并动态选择指令消耗的处理器资源。 它将处理器资源单元和资源组的管理委托给资源管理器。 资源管理器负责选择指令消耗的资源单元。 例如,如果指令消耗资源组的 1 个周期,则资源管理器从该组中选择一个可用单元; 默认情况下,资源管理器使用轮询选择器来保证资源使用在组的所有单元之间均匀分布。

llvm-mca 的调度器在内部将指令分为三组

  • WaitSet(等待集):一组操作数未就绪的指令。

  • ReadySet(就绪集):一组准备好执行的指令。

  • IssuedSet(已发射集):一组正在执行的指令。

根据操作数的可用性,分发到调度器的指令要么放入 WaitSet,要么放入 ReadySet。

每个周期,调度器都会检查是否可以将指令从 WaitSet 移动到 ReadySet,以及是否可以将 ReadySet 中的指令发射到底层管道。 该算法优先处理较旧的指令而不是较新的指令。

写回和退役阶段

已发射的指令从 ReadySet 移动到 IssuedSet。 在那里,指令等待直到它们到达写回阶段。 此时,它们从队列中删除,并通知退役控制单元。

当指令被执行时,退役控制单元将指令标记为“准备退役”。

指令按程序顺序退役。 寄存器文件收到退役通知,以便它可以释放在寄存器重命名阶段为指令分配的物理寄存器。

加载/存储单元和内存一致性模型

为了模拟内存操作的乱序执行,llvm-mca 利用模拟的加载/存储单元 (LSUnit) 来模拟加载和存储的推测执行。

每个加载(或存储)都消耗加载(或存储)队列中的一个条目。 用户可以分别指定标志 -lqueue-squeue 来限制加载队列和存储队列中的条目数。 默认情况下,队列是无限制的。

LSUnit 为内存加载和存储实现了一种宽松的一致性模型。 规则如下:

  1. 只有当两个加载之间没有插入存储或屏障时,才允许较新的加载越过较旧的加载。

  2. 如果较新的加载与较旧的存储不别名,则允许较新的加载越过较旧的存储。

  3. 不允许较新的存储越过较旧的存储。

  4. 不允许较新的存储越过较旧的加载。

默认情况下,LSUnit 乐观地假设加载不别名 (-noalias=true) 存储操作。 在此假设下,始终允许较新的加载越过较旧的存储。 本质上,LSUnit 不尝试运行任何别名分析来预测加载和存储何时彼此不别名。

请注意,在写合并内存的情况下,规则 3 可以放宽以允许重新排序非别名存储操作。 话虽如此,目前,没有办法进一步放宽内存模型(-noalias 是唯一选项)。 本质上,没有选项可以指定不同的内存类型(例如,回写、写合并、直写等),因此也就无法削弱或加强内存模型。

其他限制是:

  • LSUnit 不知道何时可能发生存储到加载的转发。

  • LSUnit 对缓存层次结构和内存类型一无所知。

  • LSUnit 不知道如何识别串行化操作和内存栅栏。

LSUnit 不尝试预测加载或存储是否命中或未命中 L1 缓存。 它只知道指令是否“MayLoad”(可能加载)和/或“MayStore”(可能存储)。 对于加载,调度模型提供了“乐观的”加载到使用延迟(通常与 L1D 中命中的加载到使用延迟相匹配)。

llvm-mca 本身并不知道串行化操作或类似内存屏障的指令。 LSUnit 过去保守地使用指令的“MayLoad”、“MayStore”和未建模的副作用标志来确定指令是否应被视为内存屏障。 这通常是不准确的,并且已更改为现在每个指令都有一个 IsAStoreBarrier 和 IsALoadBarrier 标志。 这些标志是 mca 特有的,对于每个指令默认为 false。 如果任何指令应该设置这些标志中的任何一个,则应在目标的 InstrPostProcess 类中完成。 例如,请参阅 X86InstrPostProcess::postProcessInstruction 方法,该方法位于 llvm/lib/Target/X86/MCA/X86CustomBehaviour.cpp 中。

加载/存储屏障消耗加载/存储队列的一个条目。 加载/存储屏障强制加载/存储的排序。 较新的加载无法越过加载屏障。 此外,较新的存储无法越过存储屏障。 较新的加载必须等待内存/加载屏障执行。 当加载/存储屏障成为加载/存储队列中最旧的条目时,它就会被“执行”。 这也意味着,根据构造,所有较旧的加载/存储都已执行。

总之,完整的加载/存储一致性规则是:

  1. 存储不得越过之前的存储。

  2. 存储不得越过之前的加载(无论是否设置 -noalias)。

  3. 存储必须等到较旧的存储屏障完全执行。

  4. 加载可以越过之前的加载。

  5. 除非设置了 -noalias,否则加载不得越过之前的存储。

  6. 加载必须等到较旧的加载屏障完全执行。

按序发射和执行

按序处理器被建模为单个 InOrderIssueStage 阶段。 它绕过分发、调度器和加载/存储单元。 一旦操作数寄存器可用且满足资源需求,指令就会被发射。 可以根据 LLVM 调度模型中 IssueWidth 参数的值在一个周期内发射多条指令。

一旦发射,指令就会移动到 IssuedInst 集合,直到准备好退役。 llvm-mca 确保写入按顺序提交。 但是,如果指令的写入操作中至少有一个 RetireOOO 属性为真,则允许指令乱序提交写入和退役。

自定义行为

由于某些指令在其调度模型中没有得到完美的表达,llvm-mca 并不总是能够完美地模拟它们。 然而,修改调度模型并不总是一个可行的选择(可能是因为指令被故意错误地建模,或者指令的行为非常复杂)。 在这些情况下,可以使用 CustomBehaviour 类来强制执行正确的指令建模(通常通过自定义数据依赖性和检测 llvm-mca 无法知道的危害)。

llvm-mca 附带一个通用 CustomBehaviour 类和多个特定于目标的 CustomBehaviour 类。 如果使用 -disable-cb 标志,或者如果不存在该目标的特定于目标的 CustomBehaviour 类,则将使用通用类。(通用类不执行任何操作。)目前,CustomBehaviour 类仅是按序管道的一部分,但计划在未来将其添加到乱序管道中。

CustomBehaviour 的主要方法是 checkCustomHazard(),它使用当前指令和管道中仍在执行的所有指令的列表来确定是否应分发当前指令。 作为输出,该方法返回一个整数,表示当前指令必须停顿的周期数(如果您不知道确切的数字,这可能是低估,值 0 表示不停顿)。

如果您想为尚不具备 CustomBehaviour 类的目标添加一个类,请参考现有的实现,了解如何设置它。 这些类在特定于目标的后端(例如 /llvm/lib/Target/AMDGPU/MCA/)中实现,以便它们可以访问后端符号。

Instrument Manager(工具管理器)

在某些架构上,某些指令的调度信息不包含识别最精确的调度类所需的所有信息。 例如,可能对调度产生影响的数据可以存储在 CSR 寄存器中。

RISCV 上的一个示例是,vtypevl 等寄存器中的值会更改向量指令的调度行为。 由于 MCA 不跟踪寄存器中的值,因此可以使用工具注释来指定这些值。

InstrumentManager 的主要函数是 getSchedClassID(),它可以访问 MCInst 以及为该 MCInst 激活的所有工具。 此函数可以使用工具来覆盖 MCInst 的调度类。

在 RISCV 上,包含 LMUL 信息的工具注释被 getSchedClassID() 用于将向量指令和活动的 LMUL 映射到描述该基本指令和活动的 LMUL 的伪指令的调度类。

自定义视图

llvm-mca 附带了多个视图,例如时间线视图和摘要视图。 这些视图是通用的,可以与大多数(如果不是全部)目标一起使用。 如果您希望向 llvm-mca 添加新视图,并且它不需要任何尚未通过 MC 层类(MCSubtargetInfo、MCInstrInfo 等)公开的后端功能,请将其添加到 /tools/llvm-mca/View/ 目录。 但是,如果您的新视图是特定于目标的,并且需要未公开的后端符号或功能,则可以在 /lib/Target/<TargetName>/MCA/ 目录中定义它。

要启用此特定于目标的视图,您将必须使用此目标的 CustomBehaviour 类来覆盖 CustomBehaviour::getViews() 方法。 这些方法有 3 个变体,具体取决于您希望视图在输出中显示的位置:getStartViews()getPostInstrInfoViews()getEndViews()。 这些方法返回视图向量,因此您将希望返回一个向量,其中包含目标的所有特定于目标的视图。

由于这些特定于目标(和后端依赖)的视图需要 CustomBehaviour::getViews() 变体,因此如果使用 -disable-cb 标志,则不会启用这些视图。

启用这些自定义视图不会影响非自定义(通用)视图。 继续使用常用的命令行参数来启用/禁用这些视图。