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)

不適用

不適用

不適用

<v x 1 x i64>

<v x 2 x i64>

<v x 4 x i64>

<v x 8 x i64>

i32

不適用

不適用

<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

不適用

<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)

不適用

不適用

不適用

<v x 1 x double>

<v x 2 x double>

<v x 4 x double>

<v x 8 x double>

float

不適用

不適用

<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

不適用

<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>)

遮罩向量類型

遮罩向量實際上使用向量暫存器中密集打包的位元佈局來表示。它們映射到以下 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 內函數與目標無關,因此可以從中間端的其他優化遍歷(如循環向量化器)發出。它們還支持固定長度向量類型。

    VP 內函數也沒有 passthru 操作數,但可以通過在 @llvm.vp.merge 中使用輸出來模擬尾部/遮罩不受干擾的行為。它將被降低為 vmerge,但將通過 RISCVDAGToDAGISel::performCombineVMergeAndVOps 合併回基礎指令的遮罩。

下表總結了上述表示的不同屬性

AVL

遮罩

傳遞

可縮放向量

固定長度向量

與目標無關

LLVM IR 指令

始終為 VLMAX

RVV 內函數

VP 內函數

是 (EVL)

SelectionDAG 降低

對於大多數常規可縮放向量 LLVM IR 指令,它們對應的 SelectionDAG 節點在 RISC-V 上是合法的,並且不需要任何自定義降低。

t5: nxv4i32 = add t2, t4

RISC-V 向量內函數也不需要任何自定義降低。

t12: nxv4i32 = llvm.riscv.vadd TargetConstant:i64<10056>, undef:nxv4i32, t2, t4, t6

固定長度向量

因為沒有固定長度向量模式,所以需要自定義降低固定長度向量,並在可縮放的“容器”類型中執行

  1. 固定長度向量操作數使用 insert_subvector 節點插入到可縮放容器中。容器類型經過選擇,使其最小尺寸適合固定長度向量(請參閱 getContainerForFixedLengthVector)。

  2. 然後,透過**VL(向量長度)節點**對容器類型執行操作。這些是在 RISCVInstrInfoVVLPatterns.td 中定義的自訂節點,它們反映了目標不可知的 SelectionDAG 節點,以及一些 RVV 指令。它們包含一個 AVL 操作數,設定為固定長度向量中的元素數量。某些節點還具有 passthru 或 mask 操作數,在降低固定長度向量時,這些操作數通常會設定為 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 或 mask 操作數,對於固定長度向量,這些操作數通常會設定為 undef 和全 1。

負責包裝和解包的 insert_subvectorextract_subvector 節點將被合併,最終我們會將所有固定長度向量類型都降低為可縮放的。請注意,函數介面處的固定長度向量會在可縮放向量容器中傳遞。

備註

唯一能透過降低操作傳遞的 insert_subvectorextract_subvector 節點是那些可以作為精確子暫存器插入或提取執行的節點。這表示任何未經合法化的固定長度向量 insert_subvectorextract_subvector 節點都必須位於暫存器群組邊界上,因此確切的 VLEN 必須在編譯時已知(即使用 -mrvv-vector-bits=zvl-mllvm -riscv-v-vector-bits-max=VLEN 編譯,或者具有精確的 vscale_range 屬性)。

向量述詞內建函數

VP 內建函數也會透過 VL 節點進行自訂降低。

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

目標不可知的 SelectionDAG 節點的模式位於 RISCVInstrInfoVSDPatterns.td 中,VL 節點位於 RISCVInstrInfoVVLPatterns.td 中,而 RVV 內建函數則位於 RISCVInstrInfoVPseudos.td 中。

遮罩模式

對於遮罩虛擬指令,在指令選擇期間,遮罩運算元會透過一個黏合的 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 用於向量暫存器(v0v1、…、v32)。在 \(\text{LMUL} \leq 1\) 和遮罩暫存器時使用。

  • VRM2 用於長度為 2 的向量群組,即 \(\text{LMUL}=2\)v0m2v2m2、…、v30m2

  • VRM4 用於長度為 4 的向量群組,即 \(\text{LMUL}=4\)v0m4v4m4、…、v28m4

  • VRM8 用於長度為 8 的向量群組,即 \(\text{LMUL}=8\)v0m8v8m8、…、v24m8

\(\text{LMUL} \lt 1\) 類型和遮罩類型沒有專用的類別,因此在這些情況下會使用 VR

某些指令具有限制,即寄存器運算元不能是 V0 或與 V0 重疊,因此對於這些情況,我們也有 VRNoV0 變體。

RISCVInsertVSETVLI

在向量寄存器被分配後,RISCVInsertVSETVLI 階段將為虛擬指令插入必要的 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 階段會展開 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

另請參閱