vlambda博客
学习文章列表

用LLVM写一个芯片编译器:一文读懂编译器基本概念

一、绪论
用LLVM写一个芯片编译器:一文读懂编译器基本概念


芯片是一个硬件,接收的是二进制的指令,要想让自己的编程语言编程指令,就需要一个编译器。


这个部分的重要程度丝毫不亚于芯片本身。最近国内很多公司在做AI芯片,经常出现芯片很快就做出来了,但芯片受限于编译器无法发挥最大能效的窘境。总之,了解编译器还是很重要的。


这个系列讲讲如何用LLVM做一个最简单的编译器。万变不离其宗,其他复杂的编译器可以从这个例子上拓展。


本部分主要讲基础知识,不需要了解细节,但是对编译器整体如何工作的要有概念。


不专业做编译器,难免有错误,欢迎指出,另外,部分图来自网络。


用LLVM写一个芯片编译器:一文读懂编译器基本概念
二、LLVM是个什么东西
用LLVM写一个芯片编译器:一文读懂编译器基本概念


首先一个东西要搞明白,为什么要用LLVM?LLVM的是什么?


LLVM提供了一个模块化的编译器框架,让程序员可以绕开繁琐的编译原理,快速实现一个可以运行的编译器。


常见的结构如下图。


用LLVM写一个芯片编译器:一文读懂编译器基本概念


主要由三个部分组成。


前端:将高级语言例如C或者其他语言转换成LLVM定义的中间表达方式 LLVM IR。例如非常有名的clang, 就是一个转换C/C++的前端。


中端:中端主要是对LLVM IR本身进行一下优化,输入是LLVM, 输出还是LLVM, 主要是消除无用代码等工作,一般来讲这个部分是不需要动的,可以不管他。


后端:后端输入是LLVM IR, 输出是我们的机器码。我们通常说的编译器应该主要是指这个部分。大部分优化都从这个地方实现。


至此,LLVM架构的模块化应该说的比较清楚了。很大的一个特点是隔离了前后端。


如果你想支持一个新语言,就重新实现一个前端,例如华为“仓颉”就有自己的前端来替换clang。


如果你想支持一个新硬件,那你就重行实现一个后端,让它可以正确的把LLVM IR映射到自己的芯片。


接下来我们大致讲讲前后端的流程。


用LLVM写一个芯片编译器:一文读懂编译器基本概念
三、前端在干什么
用LLVM写一个芯片编译器:一文读懂编译器基本概念


我们以Clang举例,前端主要实现四件事。


用LLVM写一个芯片编译器:一文读懂编译器基本概念


经过词法分析、语法分析、语义分析、LLVM IR生产,最终将C++转化成后端认可的LLVM IR。


词法分析:将编程语言取出一个个词,遇到不认识的字符就报错。例如将a=b+c 拆成a,= ,b ,+, c


语法分析:将语法提取出来,例如你写了个a+b=c, 明显不符合语法,直接报错


语义分析:分析一下你写的代码实际含义是不是对,例如a=b+c, a,b,c有没有定义,类型是不是对的


LLVM IR生产:经过上述三步,将你写的代码转化成树状描述(抽象语法树),然后再转化成IR定义的IR即可。


举个直观的栗子,你写的C++


// add.cpp

int add(int a, int b) {

return a + b;

}


生产的LLVM IR


(这个地方你不需要看懂每个细节,知道大概想类汇编的语言就行了, 专业的形式叫SSA, Static Single Assignment (SSA)


; ModuleID = 'add.cpp'

source_filename = "add.cpp"

target datalayout = "e-m:o-i64:64-f80:128-n8:16:32:64-S128"

target triple = "x86_64-apple-macosx10.15.0"


; Function Attrs: noinline nounwind optnone ssp uwtable

define i32 @_Z3addii(i32, i32) #0 {  

%3 = alloca i32, align 4  

%4 = alloca i32, align 4  

store i32 %0, i32* %3, align 4  

store i32 %1, i32* %4, align 4  

%5 = load i32, i32* %3, align 4  

%6 = load i32, i32* %4, align 4  

%7 = add nsw i32 %5, %6  

ret i32 %7}


用LLVM写一个芯片编译器:一文读懂编译器基本概念
四、后端在干什么
用LLVM写一个芯片编译器:一文读懂编译器基本概念


后端把你的LLVM转换成真正的汇编(或者机器码)。主要的流程如下。这个我们要重点讲讲,因为后续我们就是要实现一个这个东西支持一个新的芯片。


用LLVM写一个芯片编译器:一文读懂编译器基本概念


按步骤一个一个讲。


4.1 DAG Lowering


这个主要负责将你的LLVM IR转换为有向无环图,便于后续利用图算法优化。


例如将下面的LLVM IR 转换成图,每个节点是一个指令。


用LLVM写一个芯片编译器:一文读懂编译器基本概念

用LLVM写一个芯片编译器:一文读懂编译器基本概念


4.2 DAG Legalization


DAG图合法化,3.1中的DAG图都是LLVM IR指令,但实际上LLVM IR指令不可能被芯片全部支持,这个步骤就是替换这些不合法的指令。


4.3 Instruction Selection


这个步骤其实和3.2算是一起的功能,都是为了将LLVM IR转换成机器支持的Machine DAG.


用LLVM写一个芯片编译器:一文读懂编译器基本概念


如上图,将store换成机器仍可的st, 将16位的寄存器转向32位。一切向机器指令靠拢。


4.4 Scheduling


这个步骤主要是调整指令顺序的,从有向无环图再展开成顺序的指令。


例如把下面的指令调成这样的。


用LLVM写一个芯片编译器:一文读懂编译器基本概念

用LLVM写一个芯片编译器:一文读懂编译器基本概念


把%C的store提前一些,因为下一条ld要用C啦。


4.5 SSA-based Machine Code Optimization


这一步骤主要是做一些公共表达式合并啊去除的操作。


4.6 Register Allocation


这一步就要分配寄存器了。在3.5之前我们认为寄存器其实是可以无限用的,但实际硬件的寄存器有限的。所以我们得考虑寄存器数量与寄存器值的生命周期,将虚拟的寄存器替换成实际的寄存器。这个一般会用到图着色等等算法,贼复杂,好在LLVM都实现好了,不用在重复造轮子。


例如一个芯片,有32个可用的寄存器,如果函数使用到了64个,多的就只能压如堆栈或者等着了。


4.7 Prologue/Epilogue Code Insertion


这个主要是加上函数调用前的指令和函数结束后的指令。主要是调用前把参数存下来,调用后把结果写到固定的寄存器里。


4.8 Peephole optimizations


这个步骤主要是对代码再最后抢救一番。比如把x*2换成x<1


再比如下面这样


用LLVM写一个芯片编译器:一文读懂编译器基本概念


将两个32bit的存储换成一个64bit的存储


4.9 Code Emission


最后一步显然,将上述优化好的中间格式转换成我们真正需要的汇编,由汇编器翻译成机器码,大功告成。


用LLVM写一个芯片编译器:一文读懂编译器基本概念
五、总结


这篇文章,我们介绍了程序编译的最基本概念,编译中的大部分流程都有所涉及。下一章开始我们介绍如何用LLVM快捷的实现上面的流程。LLVM的精髓就在于,你不必对上面每一个步骤内部如何实现的彻底了解细节。你只需要知道有这么个东西就能很快攒出你的编译器。



本文来源:https://zhuanlan.zhihu.com/p/474933058


本文内容仅代表作者观点,不代表平台观点。

如有任何异议,欢迎联系我们。

如有侵权,请联系删除。



往期 精彩 回顾





2021年的第一场雪!英特尔2020年Q4财报解读



可能是DFT最全面的入门介绍


市场需求因素推动物联网多重技术创新


可追溯性,虽不熟悉但很重要