LLVM Backend Practices - Part 1
PostEncoderMethod
Background
一般情况下我们对于指令encoding采取传统的在指令定义的tablegen文件里,设置好指令的field mapping即可,如果新一代指令集有新指令,则定义新的Inst和fieldmap类即可。
在实际项目中,我们遇到过这种情况:架构演进过程中,每代之间指令功能变动不大,但指令encoding变动频繁,此外encoding采取的并不是顺序编码,而是逐bit的映射,目的是为了获取一定的指令shrink机会,即可变长指令。这里先不展开讨论shrink,而是着重讨论我们是如何解决encoding问题的。
Solution
解决方案整体上可以一句话概括:自动代码生成 + LLVM基础设施中的PostencodeMethod hook
Code Auto-gen
针对每一代架构指令集,定义一张大表,可以是csv表格或其他便于非研发人员编辑与研发人员读取的格式均可,这个表格中定义每一个指令field对应encoding的比特位序列。
读取表格,针对每一类相同编码规则的指令,自动生成形似下述代码的encoder methods。
1 | unsigned XXXInst1PostEncoder(const MCInst &MI, unsigned EncodedValue, const MCSubtargetInfo &STI) { |
LLVM Infrastrcture - PostEncoderMethod
LLVM tablegen类Instruction
中包含成员PostEncoderMethod
,对需要使用postencoder的指令类绑定相应的method,即可完成绑定,例如ARM架构中的类似代码:
1 | class NDataI<dag oops, dag iops, Format f, InstrItinClass itin, |
这里为NDataI
这类指令,绑定了一个Post encoder method,用于在code emitting时对encoding进行修改。
Shrink
Shrink操作并不少见,很多可变长指令集都有对encoding的shrink操作,即在指令编码阶段,根据指令集编码的定义,允许按照一定规则将指令编码进一步缩短。根据指令集特点,会有不同的shrink策略。当然也有一些架构代码中定义了类似shrink的pass,会做一些target specific的指令替换或立即数优化,这与我们工作中遇到的shrink不相同,以下会称之为“某架构”。
某架构最长支持128bit,最短32bit编码,指令各个field根据一些经验和profiler数据,将编码的bit位分布在不同的dword上,并且会给这些域定义一个缺省值,这样就可以根据128bit中4个dword的u32值来判断某条指令实际是否会占据更高32bit的bit位,从而帮助编译器判断是否可以做shrink。
根据以上描述,编译器会根据某条指令初始编码中的4个u32值,是否为默认值,来判断最短可以shrink到几个dword,并且在实际占据的若干个dword的最后一个的最后一位上,设置一个endbit,即简单设为1,舍去后续的encoding,即可完成shrink。
Inlined ptx/asm Impl
llvm有支持inline对应架构的asm汇编的基础设施,具体是定义一个继承MCAsmInfo
类,做一些简单的配置和注册即可初步使能inline asm,当然前提是指令定义是tablegen中要定义好每个指令对应的汇编格式。以AMDGPU为例:
- 定义并配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31// definition and configurations
AMDGPUMCAsmInfo::AMDGPUMCAsmInfo(const Triple &TT,
const MCTargetOptions &Options) {
CodePointerSize = (TT.getArch() == Triple::amdgcn) ? 8 : 4;
StackGrowsUp = true;
HasSingleParameterDotFile = false;
//===------------------------------------------------------------------===//
MinInstAlignment = 4;
// This is the maximum instruction encoded size for gfx10. With a known
// subtarget, it can be reduced to 8 bytes.
MaxInstLength = (TT.getArch() == Triple::amdgcn) ? 20 : 16;
SeparatorString = "\n";
CommentString = ";";
InlineAsmStart = ";#ASMSTART";
InlineAsmEnd = ";#ASMEND";
//===--- Data Emission Directives -------------------------------------===//
UsesELFSectionDirectiveForBSS = true;
//===--- Global Variable Emission Directives --------------------------===//
HasAggressiveSymbolFolding = true;
COMMDirectiveAlignmentIsInBytes = false;
HasNoDeadStrip = true;
//===--- Dwarf Emission Directives -----------------------------------===//
SupportsDebugInformation = true;
UsesCFIWithoutEH = true;
DwarfRegNumForCFI = true;
UseIntegratedAssembler = false;
} - 注册
1
2
3
4
5extern "C" LLVM_EXTERNAL_VISIBILITY void LLVMInitializeAMDGPUTargetMC() {
...
RegisterMCAsmInfo<AMDGPUMCAsmInfo> X(*T);
...
}
那么inline ptx怎么做呢?其实也可以利用该机制,将ptx视为某个非ptx target所能识别的汇编,但要自定义ptx汇编语句的lexer、parser,并将inlined ptx codegen成llvm ir,所以这就要求将这个特殊的inlined asm处理的pass加在llvm ir阶段。
由于ptx其实功能非常繁多,直接generate成llvm ir,ir builder的开发量会比较大,并且有一些ptx指令功能其实是比较复杂的,因此我们也可以在llvm ir生成过程中,通过将ptx指令逻辑用c语言实现,放进libdevice库中,而在ir builder时直接生成libdevice function的call inst即可一定程度上提高实现的效率。同样,这也要求我们把pass加在llvm ir阶段,并且在always inliner之前,这样可以让libdevice function call自动inline。
这个实现方案有一个限制,就是编译器不太好区分是inline ptx还是inline native asm,此时需要牺牲掉inline ptx的语法检查功能,将无法解析的inline汇编语法认为是inline native asm,交给下一步inline native asm去处理。