什么是LLVM intrinsic
简单来说,intrinsic是一种函数的统称,这种函数是一种可以让高级语言(如C/C++)直接调用汇编指令的接口或映射。例如在RISCV V扩展中,C语言程序可以通过引入头文件,调用响应函数直接访问相应的V扩展向量指令。
为LLVM添加intrinsic函数的具体流程
要想知道添加intrinsic函数的具体流程,就要对LLVM编译生成汇编指令的流程有一个初步的了解。
LLVM编译代码文件时,大致涉及到三类文件,分别是,代码源文件,LLVM IR文件 以及 最后生成的汇编(.ll)文件
因此想要添加intrinsic支持,我们也需要分别对生成IR和生成汇编的两个过程进行处理,这两个过程分别对应 clang 和 LLVM 两部分。
我们由前端clang开始到后端LLVM进行intrinsic的添加工作。
我们以中科院软件所PLCT实验室的RVV-LLVM为例,为其添加指令vleff的intrinsic支持。
Clang 部分
1. riscv_vector.td
添加intrinsic的工作从高级语言C/C++的调用开始,生成调用方法所需的头文件
在RVV-LLVM中,调用用到的头文件为#include<riscv_vector.h>
, 因此需要先在该头文件内添加函数声明,但是RVV-LLVM已经在仓库中定义了相应生成头文件TableGen脚本。因此我们只需在脚本中添加相应的结构定义就可以了
具体TableGen脚本的使用方法和语法规范请参考 TableGen 简介及使用。
生成riscv_vector.h
的脚本位于clang/include/clang/Basic/riscv_vector.td
路径下。
代码比较冗长,需要通过一两个例子来理解整体的生成逻辑。
// riscv_vector.td
foreach I = AllVectorType in {
def "vle"#I.SEW#"ff_v"#I.Name : Inst<"vle"#I.SEW#"ff",
[I.Value, I.ElemType.Value#"C*"], "_v", I.Name, 0, [-1, 0]>;
def "vle"#I.SEW#"ff_v"#I.Name#"_mask" : Inst<"vle"#I.SEW#"ff",
[I.Value, I.BoolValue, I.Value, I.ElemType.Value#"C*"], "_v", I.Name, 1, [-1, 0, 2]>;
}
以上代码是生成vleff相应头文件声明的代码,在这里,需要了解AllVectorType
和Inst
,对应的代码如下:
// riscv_vector.td
class Inst<string name, list<string> types, string inf, string suf,
int is_mask, list<int> anytype_operands, code definition = ""> {
string IntrinsicName = name;
list<string> BuiltinStr = types;
string Infix = inf;
string Suffix = suf;
// The list of indexes for the overload type. Use '-1' to refer to result
// type, '0' to refer to the first argument type and so on. This is used
// to generate clang builtin codegen snippet. The given indexes must be
// consisted with llvm/include/llvm/IR/IntrinsicsRISCV.td. For an example,
// the index list of
// class V_VX : Intrinsic<[ llvm_anyvector_ty ],
// [ LLVMMatchType<0>, llvm_anyint_ty ],
// [ IntrNoMem ]>;
// should be:
// [-1, 1]
list<int> AnyTypeOperands = anytype_operands;
int Mask = is_mask;
// If not empty, the emitter will use it to define the intrinsic function.
// Otherwise, the emitter will generate intrinsic in the default way.
code CustomDef = definition;
// Should emit BUILTIN macro. Set to false if the intrinsic function can
// be implemented by other existing builtin functions.
bit ShouldEmitBuiltin = 1;
}
AllVectorType
指的就是所有RISCV V的向量类型,例如i8mf8
`i8mf4`等。用来对应vleff doc文档中定义的函数名。
Inst
类中,Infix
应该对应v扩展intrinsic的标识_v
,Suffix
对应相应的向量类型,BuiltinStr
对应文档中定义的函数的参数。
改TableGen文件运行会生成相应的riscv_vector.h
。
2. RISCVVectorEmitter.cpp
上一步修改完TableGen后,需要检查riscv_vector_builtins.inc
和riscv_vector_builtin_cg.inc
两个文件。检查发现riscv_vector_builtin_cg.inc
调用的响应函数有误,vload是指令vle的intrinsic使用的,因此我们需要为vleff创建新的intrinsic函数。
case RISCV::BI__builtin_riscv_vle32ff_v_u32mf2:
{
Function *F = CGM.getIntrinsic(Intrinsic::riscv_vload, {ResultType, Ops[0]->getType()});
return Builder.CreateCall(F, Ops);
}
于是修改clang/utils/TableGen/RISCVVectorEmitter.cpp
。改文件中,主要修改的是函数Intrinsic::createStatementInCase()
,这个函数用于生成riscv_vector_builtin_cg.inc
中intrinsic函数的调用。
根据该函数的逻辑,添加如下代码
// case of command "vle*ff"
if(std::string(Name.begin(), NameIter-1)=="vle"
&& std::string(Name.end()-2, Name.end())=="ff"){
result += "_faultfirst";
}
编译之后生成riscv_vector_builtin_cg.inc
中代码为:
case RISCV::BI__builtin_riscv_vle32ff_v_u32mf2:
{
Function *F = CGM.getIntrinsic(Intrinsic::riscv_vload_faultfirst, {ResultType, Ops[0]->getType()});
return Builder.CreateCall(F, Ops);
}
3. IntrinsicsRISCV.td
llvm/include/llvm/IR/IntrinsicsRISCV.td
编译生成IntrinsicsRISCV.h
,定义了LLVM支持的全部intrinsic函数,因为我们在之前使用了两个个全新的intrinsic函数因此需要新定义两条intrinsic函数(_faultfirst
和_faultfirst_mask
)。
添加的定义如下:
def "_faultfirst" : Intrinsic<[llvm_anyvector_ty],
[llvm_anyptr_ty],
[IntrReadMem]>;
def "_faultfirst_mask" : Intrinsic<[llvm_anyvector_ty],
[llvm_anyvector_ty, LLVMMatchType<0>,
llvm_anyptr_ty],
[IntrReadMem]>;
4. 测试Clang部分,生成IR
添加测试文件clang/test/CodeGen/riscv-vector-intrinsics/vle8ff_v.c
,引入头文件<riscv_vector.h>
,调用相应函数:
// RUN: %clang_cc1 -triple riscv64-unknown-linux-gnu -target-feature +experimental-v -fallow-half-arguments-and-returns -fnative-half-type -S -emit-llvm %s -o - | FileCheck %s
#include <riscv_vector.h>
vint8mf8_t test_vle8ff_v_i8mf8 (int8_t *base) {
// CHECK-LABEL: test_vle8ff_v_i8mf8
// CHECK: %{{.*}} = call <vscale x 1 x i8> @llvm.riscv.vload.faultfirst{{.*}}nxv1i8{{.*}}(i8* %{{.*}})
return vle8ff_v_i8mf8(base);
}
重新编译LLVM,用LLVM编译vle8ff_v.c
:~/rvv-llvm/build/bin/clang -cc1 -internal-isystem ./build/lib/clang/12.0.0/include -triple riscv64-unknown-linux-gnu -target-feature +experimental-v -S -emit-llvm -o0 ./clang/test/CodeGen/riscv-vector-intrinsics/vle8ff_v.c
生成相应的IR,位于根目录的0文件内:
顺利生成,即证明clang部分完成。
LLVM 部分
1. RISCVPseudoInfoV.td
LLVM部分相对简单,主要涉及llvm/lib/Target/RISCV/RISCVPseudoInfoV.td
的两处修改。
添加如下两段内容:
// vleff
let Uses = [VL, VTYPE], SEWIndex = 4, MergeOpIndex = 1,
BaseInstr = !cast<Instruction>("VLE"#sew#"FF_V") in
def PseudoVLE#sew#FF_V_M#lmul
: Pseudo<(outs I.RegClass:$rd), (ins I.RegClass:$merge,
GPR:$rs1, VMaskOp:$mask, ixlenimm:$sew), []>,
RISCVVectorPseudo;
def : Pat<(I.Type (int_riscv_vload_faultfirst i64:$rs1)),
(I.Type (!cast<Instruction>("PseudoVLE"#I.SEW#"FF_V_M"#str)
(I.Type zero_reg), i64:$rs1, (I.Mask zero_reg),
I.SEW))>;
测试LLVM部分
添加测试文件 llvm/test/CodeGen/RISCV/rvv/vle8ff_v.ll
,将Clang部分生成的IR粘贴进去
; RUN: llc -mtriple=riscv64 -mattr=+experimental-v < %s | FileCheck -check-prefix=CHECK %s
; Function Attrs: noinline nounwind optnone
define <vscale x 1 x i8> @test_vle8ff_v_i8mf8(i8* %base) {
entry:
; CHECK-LABEL: vle8ff_v_i8mf8
; CHECK: vle8ff.v {{v[0-9]+}}, ({{a[0-9]+}})
%call = tail call <vscale x 1 x i8> @llvm.riscv.vload.faultfirst.nxv1i8.p0i8(i8* %base)
ret <vscale x 1 x i8> %call
}
重新编译LLVM,用LLVM编译vle8ff_v.ll :./build/bin/llc -mtriple=riscv64 -mattr=+experimental-v -verify-machineinstrs < ./llvm/test/CodeGen/RISCV/rvv/vle8ff_v.ll
顺利运行,输出正确的汇编文件,即证明LLVM部分完成。