RISC-V 向量扩展¶
RISC-V 目标架构支持 RISC-V 向量扩展 (RVV) 的 1.0 版本。本指南概述了它在 LLVM IR 中是如何建模的,以及后端如何为其生成代码。
映射到 LLVM IR 类型¶
RVV 添加了 32 个 VLEN 大小的寄存器,其中 VLEN 对于编译器来说是一个未知的常量。为了能够表示 VLEN 大小的值,RISC-V 后端采用了与 AArch64 的 SVE 相同的方法,并使用了可伸缩向量类型。
可伸缩向量类型的形式为 <vscale x n x ty>
,表示一个向量,其元素个数是 n
的倍数,元素类型为 ty
。在 RISC-V 上,n
和 ty
分别控制 LMUL 和 SEW。
LLVM 仅支持 ELEN=32 或 ELEN=64,因此 vscale
定义为 VLEN/64(参见 RISCV::RVVBitsPerBlock
)。请注意,这意味着 VLEN 必须至少为 64,因此目前不支持 VLEN=32。
LMUL=⅛ |
LMUL=¼ |
LMUL=½ |
LMUL=1 |
LMUL=2 |
LMUL=4 |
LMUL=8 |
|
---|---|---|---|---|---|---|---|
i64 (ELEN=64) |
N/A |
N/A |
N/A |
<v x 1 x i64> |
<v x 2 x i64> |
<v x 4 x i64> |
<v x 8 x i64> |
i32 |
N/A |
N/A |
<v x 1 x i32> |
<v x 2 x i32> |
<v x 4 x i32> |
<v x 8 x i32> |
<v x 16 x i32> |
i16 |
N/A |
<v x 1 x i16> |
<v x 2 x i16> |
<v x 4 x i16> |
<v x 8 x i16> |
<v x 16 x i16> |
<v x 32 x i16> |
i8 |
<v x 1 x i8> |
<v x 2 x i8> |
<v x 4 x i8> |
<v x 8 x i8> |
<v x 16 x i8> |
<v x 32 x i8> |
<v x 64 x i8> |
double (ELEN=64) |
N/A |
N/A |
N/A |
<v x 1 x double> |
<v x 2 x double> |
<v x 4 x double> |
<v x 8 x double> |
float |
N/A |
N/A |
<v x 1 x float> |
<v x 2 x float> |
<v x 4 x float> |
<v x 8 x float> |
<v x 16 x float> |
half |
N/A |
<v x 1 x half> |
<v x 2 x half> |
<v x 4 x half> |
<v x 8 x half> |
<v x 16 x half> |
<v x 32 x half> |
(将 <v x k x ty>
理解为 <vscale x k x ty>
)
掩码向量类型¶
掩码向量在物理上使用向量寄存器中紧密packed位的布局来表示。它们被映射到以下 LLVM IR 类型
<vscale x 1 x i1>
<vscale x 2 x i1>
<vscale x 4 x i1>
<vscale x 8 x i1>
<vscale x 16 x i1>
<vscale x 32 x i1>
<vscale x 64 x i1>
具有相同 SEW/LMUL 比率的两种类型将具有相同的相关掩码类型。例如,两个不同的比较,一个在 SEW=64,LMUL=2 下,另一个在 SEW=32,LMUL=1 下,都将生成一个掩码 <vscale x 2 x i1>
。
在 LLVM IR 中的表示¶
向量指令可以在 LLVM IR 中通过三种主要方式表示
可伸缩和定长向量类型上的常规指令
%c = add <vscale x 4 x i32> %a, %b %f = add <4 x i32> %d, %e
RISC-V 向量内联函数,它镜像了 C 内联函数规范
这些有非掩码变体
%c = call @llvm.riscv.vadd.nxv4i32.nxv4i32( <vscale x 4 x i32> %passthru, <vscale x 4 x i32> %a, <vscale x 4 x i32> %b, i64 %avl )
以及掩码变体
%c = call @llvm.riscv.vadd.mask.nxv4i32.nxv4i32( <vscale x 4 x i32> %passthru, <vscale x 4 x i32> %a, <vscale x 4 x i32> %b, <vscale x 4 x i1> %mask, i64 %avl, i64 0 ; policy (must be an immediate) )
两者都允许设置 AVL,并通过 passthru 操作数控制非活动/尾部元素,但掩码变体还为掩码和
vta
/vma
策略位提供了操作数。唯一有效的类型是可伸缩向量类型。
-
%c = call @llvm.vp.add.nxv4i32( <vscale x 4 x i32> %a, <vscale x 4 x i32> %b, <vscale x 4 x i1> %m i32 %evl )
与 RISC-V 内联函数不同,VP 内联函数是目标架构无关的,因此它们可以从中间端的其他优化pass(如循环向量化器)发出。它们也支持定长向量类型。
VP 内联函数也没有 passthru 操作数,但尾部/掩码不被打扰的行为可以通过在
@llvm.vp.merge
中使用输出来模拟。它将被降低为vmerge
,但将通过RISCVDAGToDAGISel::performCombineVMergeAndVOps
合并回底层指令的掩码中。
上述表示的不同属性总结如下
AVL |
掩码 |
Passthru |
可伸缩向量 |
定长向量 |
目标架构无关 |
|
---|---|---|---|---|---|---|
LLVM IR 指令 |
始终 VLMAX |
否 |
无 |
是 |
是 |
是 |
RVV 内联函数 |
是 |
是 |
是 |
是 |
否 |
否 |
VP 内联函数 |
是 (EVL) |
是 |
否 |
是 |
是 |
是 |
SelectionDAG lowering¶
对于大多数常规的可伸缩向量 LLVM IR 指令,它们对应的 SelectionDAG 节点在 RISC-V 上是合法的,并且不需要任何自定义的 lowering。
t5: nxv4i32 = add t2, t4
RISC-V 向量内联函数也不需要任何自定义的 lowering。
t12: nxv4i32 = llvm.riscv.vadd TargetConstant:i64<10056>, undef:nxv4i32, t2, t4, t6
定长向量¶
由于没有定长向量模式,因此定长向量需要进行自定义 lowering,并在可伸缩的“容器”类型中执行
定长向量操作数通过
insert_subvector
节点插入到可伸缩容器中。容器类型的选择要使其最小尺寸能够容纳定长向量(参见getContainerForFixedLengthVector
)。然后通过 VL(向量长度)节点 在容器类型上执行操作。这些是在
RISCVInstrInfoVVLPatterns.td
中定义的自定义节点,它们镜像了目标架构无关的 SelectionDAG 节点,以及一些 RVV 指令。它们包含一个 AVL 操作数,该操作数设置为定长向量中的元素数量。一些节点还具有 passthru 或掩码操作数,在 lowering 定长向量时,这些操作数通常设置为undef
和全 1。结果通过
extract_subvector
放回定长向量中。
t2: nxv2i32,ch = CopyFromReg t0, Register:nxv2i32 %0
t6: nxv2i32,ch = CopyFromReg t0, Register:nxv2i32 %1
t4: v4i32 = extract_subvector t2, Constant:i64<0>
t7: v4i32 = extract_subvector t6, Constant:i64<0>
t8: v4i32 = add t4, t7
// is custom lowered to:
t2: nxv2i32,ch = CopyFromReg t0, Register:nxv2i32 %0
t6: nxv2i32,ch = CopyFromReg t0, Register:nxv2i32 %1
t15: nxv2i1 = RISCVISD::VMSET_VL Constant:i64<4>
t16: nxv2i32 = RISCVISD::ADD_VL t2, t6, undef:nxv2i32, t15, Constant:i64<4>
t17: v4i32 = extract_subvector t16, Constant:i64<0>
VL 节点通常具有 passthru 或掩码操作数,对于定长向量,这些操作数通常设置为 undef
和全 1。
负责包装和解包的 insert_subvector
和 extract_subvector
节点将被合并消除,最终我们将所有定长向量类型降低为可伸缩类型。请注意,函数接口处的定长向量在可伸缩向量容器中传递。
注意
唯一通过 lowering 的 insert_subvector
和 extract_subvector
节点是那些可以作为精确子寄存器插入或提取执行的节点。这意味着任何未合法化的定长向量 insert_subvector
和 extract_subvector
节点都必须位于寄存器组边界上,因此必须在编译时知道确切的 VLEN(即,使用 -mrvv-vector-bits=zvl
或 -mllvm -riscv-v-vector-bits-max=VLEN
编译,或者具有精确的 vscale_range
属性)。
向量断言内联函数¶
VP 内联函数也通过 VL 节点进行自定义 lowering。
t12: nxv2i32 = vp_add t2, t4, t6, Constant:i64<8>
// is custom lowered to:
t18: nxv2i32 = RISCVISD::ADD_VL t2, t4, undef:nxv2i32, t6, Constant:i64<8>
VP EVL 和掩码分别用于 VL 节点的 AVL 和掩码,而 passthru 设置为 undef
。
指令选择¶
vl
和 vtype
需要正确配置,因此我们不能直接选择底层的向量 MachineInstr
。而是选择伪指令,这些伪指令携带发射必要的 vsetvli
所需的额外信息。
%c:vrm2 = PseudoVADD_VV_M2 %passthru:vrm2(tied-def 0), %a:vrm2, %b:vrm2, %vl:gpr, 5 /*sew*/, 3 /*policy*/
每个向量指令在 RISCVInstrInfoVPseudos.td
中定义了多个伪指令。每个可能的 LMUL 都有每个伪指令的变体,以及一个掩码变体。因此,像 vadd.vv
这样的典型指令将具有以下伪指令
%rd:vr = PseudoVADD_VV_MF8 %passthru:vr(tied-def 0), %rs2:vr, %rs1:vr, %avl:gpr, sew:imm, policy:imm
%rd:vr = PseudoVADD_VV_MF4 %passthru:vr(tied-def 0), %rs2:vr, %rs1:vr, %avl:gpr, sew:imm, policy:imm
%rd:vr = PseudoVADD_VV_MF2 %passthru:vr(tied-def 0), %rs2:vr, %rs1:vr, %avl:gpr, sew:imm, policy:imm
%rd:vr = PseudoVADD_VV_M1 %passthru:vr(tied-def 0), %rs2:vr, %rs1:vr, %avl:gpr, sew:imm, policy:imm
%rd:vrm2 = PseudoVADD_VV_M2 %passthru:vrm2(tied-def 0), %rs2:vrm2, %rs1:vrm2, %avl:gpr, sew:imm, policy:imm
%rd:vrm4 = PseudoVADD_VV_M4 %passthru:vrm4(tied-def 0), %rs2:vrm4, %rs1:vrm4, %avl:gpr, sew:imm, policy:imm
%rd:vrm8 = PseudoVADD_VV_M8 %passthru:vrm8(tied-def 0), %rs2:vrm8, %rs1:vrm8, %avl:gpr, sew:imm, policy:imm
%rd:vr = PseudoVADD_VV_MF8_MASK %passthru:vr(tied-def 0), %rs2:vr, %rs1:vr, mask:$v0, %avl:gpr, sew:imm, policy:imm
%rd:vr = PseudoVADD_VV_MF4_MASK %passthru:vr(tied-def 0), %rs2:vr, %rs1:vr, mask:$v0, %avl:gpr, sew:imm, policy:imm
%rd:vr = PseudoVADD_VV_MF2_MASK %passthru:vr(tied-def 0), %rs2:vr, %rs1:vr, mask:$v0, %avl:gpr, sew:imm, policy:imm
%rd:vr = PseudoVADD_VV_M1_MASK %passthru:vr(tied-def 0), %rs2:vr, %rs1:vr, mask:$v0, %avl:gpr, sew:imm, policy:imm
%rd:vrm2 = PseudoVADD_VV_M2_MASK %passthru:vrm2(tied-def 0), %rs2:vrm2, %%rs1:vrm2, mask:$v0, %avl:gpr, sew:imm, policy:imm
%rd:vrm4 = PseudoVADD_VV_M4_MASK %passthru:vrm4(tied-def 0), %rs2:vrm4, %rs1:vrm4, mask:$v0, %avl:gpr, sew:imm, policy:imm
%rd:vrm8 = PseudoVADD_VV_M8_MASK %passthru:vrm8(tied-def 0), %rs2:vrm8, %rs1:vrm8, mask:$v0, %avl:gpr, sew:imm, policy:imm
注意
虽然 SEW 可以编码在操作数中,但我们需要为每个 LMUL 使用单独的伪指令,因为不同的寄存器组将需要不同的寄存器类:参见 寄存器分配。
伪指令具有用于 AVL 和 SEW 的操作数(编码为 2 的幂),以及可能适用的掩码、策略或舍入模式。Passthru 操作数与目标寄存器绑定,这将确定非活动/尾部元素。
对于应使用 VLMAX 的可伸缩向量,AVL 设置为 -1
的哨兵值。
RISCVInstrInfoVSDPatterns.td
中有用于目标架构无关的 SelectionDAG 节点的模式,RISCVInstrInfoVVLPatterns.td
中有用于 VL 节点的模式,RISCVInstrInfoVPseudos.td
中有用于 RVV 内联函数的模式。
仅对掩码进行操作的指令(如 VMAND 或 VMSBF)使用带有后缀 B1、B2、B4、B8、B16、B32 或 B64 的伪指令,其中数字是 SEW/LMUL,表示 vtype 中所需的 SEW 和 LMUL 之间的比率。这些指令始终像 EEW=1 一样运行,并且始终使用值 0 作为其 SEW 操作数。
掩码模式¶
对于掩码伪指令,在指令选择期间,掩码操作数通过粘合的 CopyToReg
节点复制到物理 $v0
寄存器
t23: ch,glue = CopyToReg t0, Register:nxv4i1 $v0, t6
t25: nxv4i32 = PseudoVADD_VV_M2_MASK Register:nxv4i32 $noreg, t2, t4, Register:nxv4i1 $v0, TargetConstant:i64<8>, TargetConstant:i64<5>, TargetConstant:i64<1>, t23:1
RISCVInstrInfoVVLPatterns.td
中的模式仅匹配掩码伪指令,以减小匹配表的大小,即使节点的掩码全为 1 并且可能是非掩码伪指令。RISCVFoldMasks::convertToUnmasked
将检测掩码是否全为 1,并将其转换为非掩码形式。
$v0 = PseudoVMSET_M_B16 -1, 32
%rd:vrm2 = PseudoVADD_VV_M2_MASK %passthru:vrm2(tied-def 0), %rs2:vrm2, %rs1:vrm2, $v0, %avl:gpr, sew:imm, policy:imm
// gets optimized to:
%rd:vrm2 = PseudoVADD_VV_M2 %passthru:vrm2(tied-def 0), %rs2:vrm2, %rs1:vrm2, %avl:gpr, sew:imm, policy:imm
注意
任何 vmset.m
都可以被视为全 1 掩码,因为 AVL 之后的尾部元素是 undef
,可以用 1 替换。
寄存器分配¶
寄存器分配在向量寄存器和标量寄存器之间拆分,向量分配先运行
$v8m2 = PseudoVADD_VV_M2 $v8m2(tied-def 0), $v8m2, $v10m2, %vl:gpr, 5, 3
注意
寄存器分配被拆分,以便 RISCVInsertVSETVLI 可以在向量寄存器分配之后,但在标量寄存器分配之前运行。它需要在标量寄存器分配之前运行,因为它可能需要创建一个新的虚拟寄存器来将 AVL 设置为 VLMAX。
在向量寄存器分配之后执行 RISCVInsertVSETVLI
对机器调度器施加的约束更少,因为它无法将指令调度到 vsetvli
之后,并且它允许我们在溢出或常量重物化期间发出进一步的向量伪指令。
向量有四个寄存器类
VR
用于向量寄存器(v0
,v1,
, …,v32
)。当 \(\text{LMUL} \leq 1\) 和掩码寄存器时使用。VRM2
用于长度为 2 的向量组,即 \(\text{LMUL}=2\) (v0m2
,v2m2
, …,v30m2
)VRM4
用于长度为 4 的向量组,即 \(\text{LMUL}=4\) (v0m4
,v4m4
, …,v28m4
)VRM8
用于长度为 8 的向量组,即 \(\text{LMUL}=8\) (v0m8
,v8m8
, …,v24m8
)
\(\text{LMUL} \lt 1\) 类型和掩码类型不会从专用类中受益,因此在它们的情况下使用 VR
。
某些指令具有约束,即寄存器操作数不能是 V0
或与 V0
重叠,因此对于这些情况,我们也有 VRNoV0
变体。
RISCVInsertVSETVLI¶
在分配向量寄存器后,RISCVInsertVSETVLI
pass 将为伪指令插入必要的 vsetvli
。
dead $x0 = PseudoVSETVLI %vl:gpr, 209, implicit-def $vl, implicit-def $vtype
$v8m2 = PseudoVADD_VV_M2 $v8m2(tied-def 0), $v8m2, $v10m2, $noreg, 5, implicit $vl, implicit $vtype
物理 $vl
和 $vtype
寄存器由 PseudoVSETVLI
隐式定义,并由 PseudoVADD
隐式使用。vtype
操作数(本例中为 209
)按照规范通过 RISCVVType::encodeVTYPE
进行编码。
RISCVInsertVSETVLI
执行数据流分析以尽可能少地发射 vsetvli
。它还将尝试最小化设置 VL 的 vsetvli
的数量,即,如果只需要更改 vtype
但不需要更改 vl
,它将发射 vsetvli x0, x0
。
伪指令展开和打印¶
在标量寄存器分配之后,RISCVExpandPseudoInsts.cpp
pass 展开 PseudoVSETVLI
指令。
dead $x0 = VSETVLI $x1, 209, implicit-def $vtype, implicit-def $vl
renamable $v8m2 = PseudoVADD_VV_M2 $v8m2(tied-def 0), $v8m2, $v10m2, $noreg, 5, implicit $vl, implicit $vtype
请注意,向量伪指令仍然保留,因为它需要编码 LMUL 的寄存器类。它的 AVL 和 SEW 操作数不再使用。
RISCVAsmPrinter
然后将伪指令降低为真正的 MCInst
。
vsetvli a0, zero, e32, m2, ta, ma
vadd.vv v8, v8, v10