简介:作者:长别
1. 前言
随着IT基础设施的发展,现代的数据处理系统需要处理更多的数据、支持更为复杂的算法。数据量的增长和算法的复杂化,为数据分析系统带来了严峻的性能挑战。近年来,我们可以在数据库、大数据系统和AI平台等领域看到很多性能优化的技术,技术涵盖体系结构、编译技术和高性能计算等领域。作为编译优化技术的代表,本文主要介绍基于LLVM的代码生成技术(简称Codeden)。
LLVM是一款非常流行的开源编译器框架,支持多种语言和底层硬件。开发者可以基于LLVM搭建自己的编译框架并进行二次开发,将不同的语言或者逻辑编译成运行在多种硬件上的可执行文件。对于Codegen技术来说,我们主要关注LLVM IR的格式以及生成LLVM IR的API。在本文的如下部分,我们首先对LLVM IR进行介绍,然后介绍Codegen技术的原理和使用场景,最后我们介绍在阿里云自研的云原生数据仓库产品AnalyticDB PostgreSQL中,Codegen的典型应用场景。
2. LLVM IR简介及上手教程
在编译器理论与实践中,IR是非常重要的一环。IR的全称叫做Intermediate Representation,翻译过来叫“中间表示”。 对于一个编译器来说,从上层抽象的高级语言到底层的汇编语言,要经历很多个环节(pass),经历不同的表现形式。而编译优化技术有很多种,每种技术作用的编译环节不同。但是IR是一个明显的分水岭。IR以上的编译优化,不需要关心底层硬件的细节,比如硬件的指令集、寄存器文件大小等。IR以下的编译优化,需要和硬件打交道。LLVM最为著名是它的IR的设计。得益于巧妙地IR设计,LLVM向上可以支持不同的语言,向下可以支持不同的硬件,而且不同的语言可以复用IR层的优化算法。

上图展示了LLVM的一个框架图。LLVM把整个编译过程分为三步:(1)前端,把高级语言转换为IR。(2)中端,在IR层做优化。(3) 后端,把IR转化为对应的硬件平台的汇编语言。因此LLVM的扩展性很好。比如你要实现一个名为toyc的语言、希望运行在ARM平台上,你只需要实现一个toyc->LLVM IR的前端,其他部分调LLVM的模块就可以了。或者你要搞一个新的硬件平台,那么只需要搞定LLVM IR->新硬件这一阶段,然后该硬件就可以支持很多种现存的语言。因此,IR是LLVM最有竞争力的地方,同时也是学习使用LLVM Codegen的最核心的地方。
2.1 LLVM IR基本知识
LLVM的IR格式非常像汇编,对于学习过汇编语言的同学来说,学会使用LLVM IR进行编程非常容易。对于没学过汇编语言的同学,也不用担心,汇编其实并不难。汇编难的不是学会,而是工程实现。因为汇编语言的开发难度,会随着工程复杂度的提升呈指数级上升。接下来我们需要了解IR中最重要的三部分,指令格式、Basic Block & CFG,还有SSA。完整的LLVM IR信息请参考https://llvm.org/docs/LangRef.html。
指令格式。LLVM IR提供了一种类似于汇编语言的三地址码式的指令格式。下面的代码片段是一个非常简单的用LLVM IR实现的函数,该函数的输入是5个i32类型(int32)的整数,函数的功能是计算这5个数的和并返回。LLVM IR是支持一些基本的数据类型的,比如i8、i32、浮点数等。LLVM IR中得变量的命名是以 "%"开头,默认%0是函数的第一个参数、%1是第二个参数,依次类推。机器生成的变量一般是以数字进行命名,如果是手写的话,可以根据自己的喜好选择合适的命名方法。LLVM IR的指令格式包括操作符、类型、输入、返回值。例如 "%6 = add i32 %0, %1"的操作符号是"add"、类型是"i32"、输入是"%0"和“%1”、返回值是"%6"。总的来说,IR支持一些基本的指令,然后编译器通过这些基本指令的来完成一些复杂的运算。例如,我们在C中写一个形如“A * B + C”的表达式在LLVM IR中是通过一条乘法和一条加法指令来完成的,另外可能也包括一些类型转换指令。
define i32 @ir_add(i32, i32, i32, i32, i32){ %6 = add i32 %0, %1 %7 = add i32 %6, %2 %8 = add i32 %7, %3 %9 = add i32 %8, %4 ret i32 %9 }
Basic Block & CFG。了解了IR的指令格式以后,接下来我们需要了解两个概念:Basic Block(基本块,简称BB)和Control Flow Graph(控制流图,CFG)。下图(左)展示了一个简单的C语言函数,下图(中)是使用clang编译出来的对应的LLVM IR,下图(右)是使用graphviz画出来的CFG。结合这张图,我们解释下Basic Block和CFG的概念。

在我们平时接触到的高级语言中,每种语言都会有很多分支跳转语句,比如C语言中有for, while, if等关键字,这些关键字都代表着分支跳转。开发者通过分支跳转来实现不同的逻辑运算。汇编语言通常通过有条件跳转和无条件跳转两种跳转指令来实现逻辑运算,LLVM IR同理。比如在LLVM IR中"br label %7"意味着无论如何都跳转到名为%7的label那里,这是一条无条件跳转指令。"br i1 %10, label %11, label %22"是有条件跳转,意味着这如果%10是true则跳转到名为%11的label,否则跳转到名为%22的label。
在了解了跳转指令这个概念后,我们介绍Basic Block的概念。一个Basic Block是指一段串行执行的指令流,除了最后一句之外不会有跳转指令,Basic Block入口的第一条指令叫做“Leading instruction”。除了第一个Basic Block之外,每个Basic Block都会有一个名字(label)。第一个Basic Block也可以有,只是有时候没必要。例如在这段代码当中一共有5个Basic Block。Basic Block的概念,解决了控制逻辑的问题。通过Basic Block, 我们可以把代码划分成不同的代码块,在编译优化中,有的优化是针对单个Basic Block的,有些是针对多个Basic Block的。
CFG(Control Flow Graph, 控制流图)其实就是由Basic Block以及Basic Block之间的跳转关系组成的一个图。例如上图所示的代码,一共有5个Basic Block,箭头列出了Basic Block之间的跳转关系,共同组成了一个CFG。如果一个Basic Block只有一个箭头指向别的Block,那么这个跳转就是无条件跳转,否则是有条件跳转。

本文深入介绍了LLVM编译框架中的代码生成技术(Codegen),特别是LLVMIR的使用和优化。LLVMIR作为中间表示,允许开发者编写类似于汇编的代码,实现跨语言和硬件平台的编译。文章通过实例展示了如何使用LLVMIR编写和理解基本块(BasicBlock)、控制流图(CFG)以及静态单赋值形式(SSA)。尽管直接使用LLVMIR编写代码开发难度高,调试复杂,但LLVMCodegen在特定场景下,如Java或Python的性能提升、硬件和语言不兼容问题以及数据库中的表达式处理,能发挥优势。在数据库领域,AnalyticDBPostgreSQL利用LLVMCodegen优化了表达式处理,提升了执行引擎性能。
最低0.47元/天 解锁文章
4032

被折叠的 条评论
为什么被折叠?



