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) |
不適用 |
不適用 |
不適用 |
<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 中可以通過三種主要方式表示
可縮放和固定長度向量類型上的常規指令
%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 內函數與目標無關,因此可以從中間端的其他優化遍歷(如循環向量化器)發出。它們還支持固定長度向量類型。
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
固定長度向量¶
因為沒有固定長度向量模式,所以需要自定義降低固定長度向量,並在可縮放的“容器”類型中執行
固定長度向量操作數使用
insert_subvector
節點插入到可縮放容器中。容器類型經過選擇,使其最小尺寸適合固定長度向量(請參閱getContainerForFixedLengthVector
)。然後,透過**VL(向量長度)節點**對容器類型執行操作。這些是在
RISCVInstrInfoVVLPatterns.td
中定義的自訂節點,它們反映了目標不可知的 SelectionDAG 節點,以及一些 RVV 指令。它們包含一個 AVL 操作數,設定為固定長度向量中的元素數量。某些節點還具有 passthru 或 mask 操作數,在降低固定長度向量時,這些操作數通常會設定為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 或 mask 操作數,對於固定長度向量,這些操作數通常會設定為 undef
和全 1。
負責包裝和解包的 insert_subvector
和 extract_subvector
節點將被合併,最終我們會將所有固定長度向量類型都降低為可縮放的。請注意,函數介面處的固定長度向量會在可縮放向量容器中傳遞。
備註
唯一能透過降低操作傳遞的 insert_subvector
和 extract_subvector
節點是那些可以作為精確子暫存器插入或提取執行的節點。這表示任何未經合法化的固定長度向量 insert_subvector
和 extract_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
。
指令選擇¶
需要正確設定 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
。
目標不可知的 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
用於向量暫存器(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
階段將為虛擬指令插入必要的 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