学习过程,求勘误,多谢指出~
今天学习如何在后端添加一个 builtin 和对应的指令 mapping 起来?
整个过程预览:
-
源代码调用:在高级语言的源代码中使用特定于 AMDGPU 的内置函数,例如就这么写 __builtin_amdgcn_sin(x)。
-
LLVM IR 生成:Clang 编译器在编译过程中将这些内置函数调用转换为 LLVM IR 中的内置函数,例如 %0 = call float @llvm.amdgcn.sin.f32(float %x)。
-
机器代码生成:LLVM 的后端(例如 AMDGPU 后端)读取 LLVM IR,并根据 IntrinsicsAMDGPU.td 文件中的定义,将这些内置函数映射到具体的机器指令,就是 amd 的 isa 指令。
1. 什么情况下会添加这个函数?
https://llvm.org/docs/ExtendingLLVM.html
在使用 LLVM 的过程中,您可能希望为您的研究项目或实验定制它。此时,您可能会意识到需要向 LLVM 添加一些内容,无论是新的基本类型、新的内部函数还是全新的指令。
一般情况时有新的 target 添加,或者是支持新的 target,那么才需要,不然似乎需要。
因为这个需要花费大量的时间,请在社区咨询和是不是已经支持,或者已经有人在做了。
2. 开始添加
向 LLVM 添加新的内部函数比添加新指令要容易得多。几乎所有 LLVM 扩展都应该作为内在函数开始,然后在必要时转换为指令。
首先,要知道 tablegen 的语法。
关于 tablegen, 可以看下这篇文章,太多了,死机是记不住的,怎么办?多在实际项目中应用,记不住的再来查字典。
https://blog.llvm.org/posts/2023-12-07-tools-for-learning-llvm-tablegen/
https://llvm.org/docs/TableGen/
https://llvm.org/docs/TableGen/ProgRef.html
1. 添加定义 builtin 和 intrinsic,为了高级语言可以调用
首先需要了解点,基础的知识。
builtin 和 intrinsic 有什么区别?
一开始也不懂,只是啥呀?看名字感觉都是内置函数,确实都是内置函数。
我理解的有点区别,就是 builtin 是高级语言级别的,而 intrinsic 是 IR 级别的。
在 llvm 里面他们的命名大概是这样子的。
- builtin:__builtin_memcpy()、__builtin_popcount()
- intrinsic: llvm.memcpy(), llvm.ctpop()
CGBuiltin.cpp 这个文件干嘛的?
CGBuiltin.cpp
文件是 LLVM 项目的一部分,主要负责生成 LLVM IR 代码以实现内置函数的编译,包括初始化内存、生成内置库函数调用以及处理特定于目标架构的内置函数。
这个文件里面主要关注 2 个函数:文件包含了初始化内存的函数 initializeAlloca
,该函数根据不同的初始化策略(如未初始化、全零初始化或模式初始化)来初始化 alloca 分配的内存。还有就是对齐方式,这个主要取决于硬件。
getBuiltinLibFunction
函数负责将高级语言中的内置函数映射到 LLVM IR 的内置函数,这对于生成有效的目标代码至关重要。
举个例子:比如高级语言是__builtin_fabs,对应的 IR 可能就是 llvm.fabs。
可以看到很多枚举值
。比如:case RISCV::BI__builtin_riscv_ntl_store:
这个枚举值是通过 TargetBuiltins.h 里面通过 namespace 里面的 include 来的。
/// AMDGPU builtins
namespace AMDGPU {
enum {
LastTIBuiltin = clang::Builtin::FirstTSBuiltin - 1,
#define BUILTIN(ID, TYPE, ATTRS) BI##ID,
#include "clang/Basic/BuiltinsAMDGPU.def"
LastTSBuiltin
};
}
Builtins*.def 是干嘛的
来到clang/Basic/BuiltinsAMDGPU.def
这个文件里面,就可以看到具体的定义 BUILTIN 定义了。
有必要对这个代码解释下,我看了之后,一头雾水。LastTIBuiltin 和 FirstTSBuiltin 是什么?
- 第一个是 namespace 这个好理解;
- enum 枚举值;匿名枚举值;
- LastTIBuiltin = clang::Builtin::FirstTSBuiltin - 1,:我看了大概是 AMDGPU 内置函数的 ID 值比 clang 内置函数的 ID 值小 1。 LastTIBuiltin:这个枚举成员表示 LLVM/Clang 中最后一个 Target-Independent (TI) 内置函数的编号。Target-Independent 内置函数是不依赖于特定硬件架构的内置函数。LastTIBuiltin 的值是 clang::Builtin::FirstTSBuiltin - 1,其中 FirstTSBuiltin 表示第一个 Target-Specific (TS) 内置函数的编号。这样设置是为了确保 AMDGPU 的内置函数枚举值在数值上紧跟在 TI 内置函数之后。
- #define BUILTIN(ID, TYPE, ATTRS) BI##ID, 定义了宏,包含 3 个参数;展开后,应该类似 BI1,BI2……
BUILTIN(__builtin_amdgcn_dispatch_ptr, "v*4", "nc")
BUILTIN(__builtin_amdgcn_kernarg_segment_ptr, "v*4", "nc")
BUILTIN(__builtin_amdgcn_implicitarg_ptr, "v*4", "nc")
BUILTIN(__builtin_amdgcn_queue_ptr, "v*4", "nc")
估计你和我一样,第一眼看到的饿时候,what?这些神秘的 v,n,c 是什么意思?没关系,看文件的顶部,有一个文件,builtins.def
,跟过去,看一下里面的内容,下面是部分内容。
//https://github.com/llvm/llvm-project/blob/main/clang/include/clang/Basic/Builtins.def
// (result value, then each argument) as follows:
// v -> void
// b -> boolean
// c -> char
// s -> short
// i -> int
// h -> half (__fp16, OpenCL)
// x -> half (_Float16)
// y -> half (__bf16)
// f -> float
// d -> double
// z -> size_t
// w -> wchar_t
// F -> constant CFString
// G -> id
// H -> SEL
// M -> struct objc_super
// a -> __builtin_va_list
// A -> "reference" to __builtin_va_list
……
// Types may be prefixed with the following modifiers:
// L -> long (e.g. Li for 'long int', Ld for 'long double')
// LL -> long long (e.g. LLi for 'long long int', LLd for __float128)
// LLL -> __int128_t (e.g. LLLi)
// Z -> int32_t (require a native 32-bit integer type on the target)
// W -> int64_t (require a native 64-bit integer type on the target)
// N -> 'int' size if target is LP64, 'L' otherwise.
// O -> long for OpenCL targets, long long otherwise.
// S -> signed
// U -> unsigned
// I -> Required to constant fold to an integer constant expression.
//
看了就知道是什么意思了吧,v 就是 void,没有返回值
。如果是Si
signed integer 就是有符号整型。
Intrinsics*.td 是干嘛的
最后这个枚举值需要定义在llvm/IR/Intrinsics*.td
这个里面,就是第一步我们定义的。
llvm/include/llvm/IR/Intrinsics*.td:
用 amdgpu 来举例:
https://github.com/llvm/llvm-project/blob/main/llvm/include/llvm/IR/IntrinsicsAMDGPU.td
下面是我为了举例自己构造的,用法不一定正确。
def int_amdgcn_rcp_legacy : ClangBuiltin<"__builtin_amdgcn_rcp_legacy">,
DefaultAttrsIntrinsic<[llvm_float_ty], [llvm_i32_ty],
[IntrNoMem, IntrSpeculatable]
>;
- IntrNoMem 这些就是属性,也是定义在 Intrinsics.td 里面的,有解释,比如这个的定义意思就是:IntrNoMem - The intrinsic does not access memory or have any other side effects. It may be CSE’d deleted if dead, etc.
- llvm_i32ty 是定义的一个数据类型属性,llvm 是命名空间的一部分,用于避免与其他库或框架中的类型名冲突。i32 表示 32 位整数,_ty 表示这是一个类型(type)的定义。
其他就去这个上面的链接上看。
顺便提一下这个 def 是啥和用法。
- def 是定义一个 record:def 的语法
Def ::= "def" [NameValue] RecordBody NameValue ::= Value (parsed in a special mode)
这里的 intrinsic def 用法这么定义的(这个定义我没有找到,为什么这么约束的?我猜应该是在 Intrinsic.td 里面)
def <name> <args> <ret> [<attrs>] {
// Definition body
}
- name>:正在定义的实体的名称,例如内部函数、指令或数据类型。
- args>:实体的参数列表,通常由其数据类型表示。
- ret>:实体的返回类型(如果适用)。
- attrs>:实体的可选属性列表,提供附加信息。 {}:包含定义主体,其中包含有关实体的特定详细信息。
这里的ClangBuiltin<"__builtin_amdgcn_rcp_legacy">
就是 NameValue,就是可以在高级语言直接调用的。
RecordBody 就是后面的DefaultAttrsIntrinsic<>
.
这个 Clangbuiltin 和 DefaultAttrsIntrinsic 都定义在:https://github.com/llvm/llvm-project/blob/main/llvm/include/llvm/IR/Intrinsics.td
// Intrinsic with default attributes (disable_default_attributes = false).
class DefaultAttrsIntrinsic<list<LLVMType> ret_types,
list<LLVMType> param_types = [],
list<IntrinsicProperty> intr_properties = [],
string name = "",
list<SDNodeProperty> sd_properties = []>
: Intrinsic<ret_types, param_types,
intr_properties, name,
sd_properties, /*disable_default_attributes*/ 0> {}
定义好了,高级语言可以条用的接口,那么接下来就是,后端的实现了。
2. 把 intrinsic 和指令关联起来
需要到 llvm 的前端把 builtin 函数转化为 intrinsic,这个是前端实现的,我不太熟悉,我用 AI 搜索下了,回答如下:
1. 语法分析
首先,LLVM 的前端分析器(如 Clang)会对源代码进行语法分析,将源代码转换为抽象语法树(AST)。AST 包含了代码的结构和语义信息,包括函数声明、函数调用、表达式等。
在 AST 中,intrinsic 函数会被表示为特殊的节点,其中包含了函数名、参数类型和返回值类型等信息。
2. 语义分析
接下来,LLVM 会进行语义分析,检查代码的语义是否正确。在这个阶段,LLVM 会检查 intrinsic 函数的调用是否合法,例如参数类型是否匹配、函数是否被定义等。
如果语义分析成功,LLVM 会将 AST 转换为 IR。
3. IR 生成
在 IR 生成阶段,LLVM 会为每个 intrinsic 函数生成对应的 IR 函数。IR 函数由一系列 LLVM IR 指令组成,这些指令描述了函数的具体操作
然后是 intrinsic 和指令的映射。
这类文件 llvm/lib/Target/AMDGPU/*Instructions.td 就是定义具体的指令的,
在这个文件里面 https://github.com/llvm/llvm-project/blob/main/llvm/lib/Target/AMDGPU/DSInstructions.td 可以看到关于 ds_consume 的多个定义。
def DS_CONSUME : DS_0A_RET<"ds_consume">;
defm DS_CONSUME : DS_Real_gfx6_gfx7_gfx10_gfx11_gfx12<0x03d>;
def DS_CONSUME_vi : DS_Real_vi<0xbd, DS_CONSUME>;
看一个,就可以看到这个类 DS_0A_RET
的定义。
class DS_0A_RET <string opName> : DS_Pseudo<opName,
(outs getLdStRegisterOperand<VGPR_32>.ret:$vdst),
(ins Offset:$offset, gds:$gds),
" $vdst$offset$gds"> {
let mayLoad = 1;
let mayStore = 1;
let has_addr = 0;
let has_data0 = 0;
let has_data1 = 0;
}
里面就是约束了定义时候的用法,其他就没有了,还有一些属性。
再来看另外一个,类名DS_Real_vi
。用的是一个 64 位的寄存器,0~63,每一位代表的是什么,都写了。
class DS_Real_vi <bits<8> op, DS_Pseudo ps> :
DS_Real <ps>,
SIMCInstr <ps.PseudoInstr, SIEncodingFamily.VI> {
let AssemblerPredicate = isGFX8GFX9;
let DecoderNamespace = "GFX8";
// encoding
let Inst{7-0} = !if(ps.has_offset0, offset0, 0);
let Inst{15-8} = !if(ps.has_offset1, offset1, 0);
let Inst{16} = !if(ps.has_gds, gds, ps.gdsValue);
let Inst{24-17} = op;
let Inst{25} = acc;
let Inst{31-26} = 0x36; // ds prefix
let Inst{39-32} = !if(ps.has_addr, addr, !if(ps.has_gws_data0, data0{7-0}, 0));
let Inst{47-40} = !if(ps.has_data0, data0{7-0}, 0);
let Inst{55-48} = !if(ps.has_data1, data1{7-0}, 0);
let Inst{63-56} = !if(ps.has_vdst, vdst{7-0}, 0);
}
我强行解读下,估计是小尾端编码:
- 0~15 位,都是表示 offset
- 16 位:是一个 gds 的值,估计是一个 bool 啥的
- 17-24:是一个操作数,opcode,每个指令不一样,是唯一的
- 25:acc 是加速?准确?不装了,确实不懂……
- 32-55 都是 data,应该是 source 之类的 addr
- 56~63 是 destination,
3. 用 amdgpu 的来举例
首先,需要 https://github.com/llvm/llvm-project/blob/main/llvm/include/llvm/IR/IntrinsicsAMDGPU.td 里面定义个 intrinsic :: amdgcn_div_fixuph
函数。必须是 int_ 开头。
def int_amdgcn_div_fixuph : DefaultAttrsIntrinsic<[llvm_anyfloat_ty],
[LLVMMatchType<0>, LLVMMatchType<0>, LLVMMatchType<0>],
[IntrNoMem, IntrSpeculatable]
>;
然后,在 https://github.com/llvm/llvm-project/blob/main/clang/include/clang/Basic/BuiltinsAMDGPU.def 文件里面定义个 builtin 的函数。
TARGET_BUILTIN(__builtin_amdgcn_div_fixuph, "hhhh", "nc", "16-bit-insts")
然后,在 https://github.com/llvm/llvm-project/blob/main/clang/lib/CodeGen/CGBuiltin.cpp 里面,然后对应的 intrinsic 函数,这里返回的Intrinsic::amdgcn_div_fixuph
。
case AMDGPU::BI__builtin_amdgcn_div_fixuph:
return emitTernaryBuiltin(*this, E, Intrinsic::amdgcn_div_fixuph);
这样前端就可以调用了,但是在 amdgpu 上还没有映射具体的指令。
这个需要在 intrinsic 里面添加这个定义。
defm V_DIV_FIXUP_F16 : VOP3Inst <"v_div_fixup_f16", VOP3_Profile<VOP_F16_F16_F16_F16>, AMDGPUdiv_fixup>;
这个对于的指令就是v_div_fixup_f16
。
所以高级语言调用__builtin_amdgcn_div_fixuph
然后根据参数,会具体映射到定义的指令上面:v_div_fixup_f16
。
end