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 上,nty 分别控制 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 中通过三种主要方式表示

  1. 可伸缩和定长向量类型上的常规指令

    %c = add <vscale x 4 x i32> %a, %b
    %f = add <4 x i32> %d, %e
    
  2. 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 策略位提供了操作数。

    唯一有效的类型是可伸缩向量类型。

  3. 向量断言 (VP) 内联函数

    %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,并在可伸缩的“容器”类型中执行

  1. 定长向量操作数通过 insert_subvector 节点插入到可伸缩容器中。容器类型的选择要使其最小尺寸能够容纳定长向量(参见 getContainerForFixedLengthVector)。

  2. 然后通过 VL(向量长度)节点 在容器类型上执行操作。这些是在 RISCVInstrInfoVVLPatterns.td 中定义的自定义节点,它们镜像了目标架构无关的 SelectionDAG 节点,以及一些 RVV 指令。它们包含一个 AVL 操作数,该操作数设置为定长向量中的元素数量。一些节点还具有 passthru 或掩码操作数,在 lowering 定长向量时,这些操作数通常设置为 undef 和全 1。

  3. 结果通过 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_subvectorextract_subvector 节点将被合并消除,最终我们将所有定长向量类型降低为可伸缩类型。请注意,函数接口处的定长向量在可伸缩向量容器中传递。

注意

唯一通过 lowering 的 insert_subvectorextract_subvector 节点是那些可以作为精确子寄存器插入或提取执行的节点。这意味着任何未合法化的定长向量 insert_subvectorextract_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

指令选择

vlvtype 需要正确配置,因此我们不能直接选择底层的向量 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

另请参阅