llvm-mca - LLVM 机器码分析器

概要

llvm-mca [选项] [输入]

描述

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

性能以吞吐量以及处理器资源消耗来衡量。该工具目前适用于 LLVM 中存在调度模型的后端处理器。

此工具的主要目标不仅是预测代码在目标机器上运行时的性能,还包括帮助诊断潜在的性能问题。

给定一段汇编代码序列,llvm-mca 会估算每周期指令数 (IPC) 以及硬件资源压力。分析和报告风格借鉴了英特尔的 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 中调度模型质量的影响。

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

选项

如果 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,而不是使用目标特定的实现。通用类创建不提供额外信息的 Instruments,并且 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,因为它们对应于 AnalysisRegions。 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)。在此过程中,管道收集大量与执行相关的统计信息。在此过程结束时,管道生成并打印从收集的统计信息生成的报告。

这是一个由该工具为两个包含四个元素的打包浮点向量点积生成的性能报告示例。分析针对目标 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 条模拟指令。模拟微操作码 (uOps) 的总数也是 900。

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

字段 DispatchWidth 是每个模拟周期调度到乱序后端的微操作码的最大数量。对于具有顺序后端的处理器,DispatchWidth 是每个模拟周期发出到后端的微操作码的最大数量。

IPC 通过将模拟指令总数除以周期总数计算得出。

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

在没有循环携带数据依赖项的情况下,观察到的 IPC 趋向于一个理论最大值,可以通过将单个迭代的指令数除以 Block RThroughput 来计算。

字段“每周期 uOps”通过将模拟微操作码总数除以周期总数计算得出。调度宽度和此字段之间的差异是性能问题的指标。在没有循环携带数据依赖项的情况下,观察到的“每周期 uOps”应趋向于一个理论最大吞吐量,可以通过将单个迭代的 uOps 数除以 Block RThroughput 来计算。

字段 uOps Per Cycle 受调度宽度的限制。这是因为调度宽度限制了调度组的最大大小。IPC 和“每周期 uOps”都受硬件并行度的限制。硬件资源的可用性会影响资源压力分布,并限制每个周期可以并行执行的指令数量。调度宽度和每周期理论最大 uOps(通过将单个迭代的 uOps 数除以 Block RThroughput 计算得出)之间的差异是由于缺乏硬件资源导致的性能瓶颈的指标。通常,Block RThroughput 越低越好。

在此示例中,uOps per iteration/Block RThroughput 为 1.50。由于没有循环携带依赖项,因此当迭代次数趋于无穷大时,观察到的 uOps Per Cycle 预计将接近 1.50。调度宽度 (2.00) 与理论最大 uOp 吞吐量 (1.50) 之间的差异是由于缺乏硬件资源导致的性能瓶颈的指标,并且资源压力视图可以帮助识别有问题的资源使用情况。

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

字段RThroughput是指令吞吐量的倒数。吞吐量计算为在没有操作数依赖的情况下,每时钟周期可以执行的相同类型指令的最大数量。在本例中,向量浮点乘法的倒数吞吐量为 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

Encoding Size列显示指令的字节大小。Encodings列显示实际的指令编码(十六进制的字节序列)。

第三部分是资源压力视图。此视图报告了每个迭代中指令为目标上的每个处理器资源单元消耗的平均资源周期数。信息结构化在两个表中。第一个表报告了每次迭代平均花费的资源周期数。第二个表将资源周期与序列中的机器指令相关联。例如,指令 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 开始依次编号。

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

  • 指令 [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显示。

下一个表退休控制单元显示一个直方图,其中显示一个计数,表示在某些周期内退休的指令的数量。在这种情况下,在 610 个模拟周期中,在同一周期内退休了两条指令 399 次(65.4%),并且有 109 个周期没有退休指令。退休统计信息可以通过使用命令选项-all-stats-retire-stats显示。

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

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

指令流

本节描述指令在llvm-mca的默认流水线中的流动,以及在此过程中涉及的功能单元。

默认流水线实现以下用于处理指令的阶段序列。

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

  • 发出(指令被发出到处理器流水线)。

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

  • 退休(指令退休;写入被体系结构提交)。

有序流水线实现以下阶段序列

  • InOrderIssue(指令被发出到处理器流水线)。

  • 退休(指令退休;写入被体系结构提交)。

llvm-mca假设所有指令都在模拟开始之前被解码并放入队列中。因此,不模拟指令获取和解码阶段。不会诊断前端的性能瓶颈。此外,llvm-mca不模拟分支预测。

指令调度

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

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

如果满足以下条件,则可以调度指令

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

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

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

  • 调度器未满。

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

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

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

指令发出

每个处理器调度器都实现了一个指令缓冲区。指令必须在调度器的缓冲区中等待,直到输入寄存器操作数可用。只有在这一点上,指令才能有资格执行,并且可以(可能无序)发出执行。指令延迟由llvm-mca在调度模型的帮助下计算。

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

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 类中完成。例如,请查看llvm/lib/Target/X86/MCA/X86CustomBehaviour.cpp中的X86InstrPostProcess::postProcessInstruction方法。

加载/存储屏障消耗加载/存储队列的一个条目。加载/存储屏障强制执行加载/存储的顺序。较新的加载不能通过加载屏障。此外,较新的存储不能通过存储屏障。较新的加载必须等待内存/加载屏障执行。加载/存储屏障在成为加载/存储队列中(或队列中)最旧的条目时“执行”。这也意味着,根据构造,所有较旧的加载/存储都已执行。

总之,加载/存储一致性规则的完整集合为

  1. 存储不得通过先前的存储。

  2. 存储不得通过先前的加载(无论-noalias如何)。

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

  4. 加载可以传递先前的加载。

  5. 加载不得传递先前的存储,除非设置了-noalias

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

有序发出和执行

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

发出后,指令将移至IssuedInst集合,直到它准备好退休。llvm-mca确保写入按顺序提交。但是,如果至少一个写入的RetireOOO属性为真,则允许指令无序提交写入和退休。

自定义行为

由于某些指令在其调度模型中表达得不够完美,llvm-mca 并不总是能够完美地模拟它们。但是,修改调度模型并不总是可行的选项(可能是因为指令是故意建模不正确,或者指令的行为非常复杂)。在这些情况下,可以使用 CustomBehaviour 类来强制执行正确的指令建模(通常是通过自定义数据依赖关系并检测 llvm-mca 无法获知的危险)。

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

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

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

仪器管理器

在某些架构上,某些指令的调度信息不包含识别最精确调度类所需的所有信息。例如,可能影响调度的數據可以存储在 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 标志,将不会启用这些视图。

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