OceanBase的表达式编译执行
1 背景
马老师曾说过:“人类正从IT时代走向DT时代”,而在DT时代,数据量的爆增,对现有系统数据处理效率提出了更高的要求,近几年,为了提升计算效率,JIT(Just-In-Time Compiler)技术得到了广泛的应用,LLVM对这些技术提供了很好的支持,并且能够很方便的供开发者使用。
为了既能很好的支持OLTP场景,同时也能对OLAP的场景(大部分执行时间是在进行表达式计算)快速响应。 我们使用LLVM提供的相关技术,实现了表达式编译执行,表达式执行速度得到大幅提升。
2 LLVM简介
LLVM是模块化、可重用的编译器和工具链技术的集合。
编译器分为前端、优化和后端,前端主要包括语法解析、语义分析以及中间代码生成,而后端则包括汇编代码、目标文件的生成以及连接等等过程。LLVM一个重要的设计就是其中间表示语言LLVM IR(the LLVM intermediate representation)。通过LLVM IR连接前端和后端,中间优化阶段均针对IR进行优化,使得优化阶段更加通用,并且前端和后端高度模块化,均可重用,不论是支持新的编程语言,还是支持新的硬件设备,都不需要对优化阶段做任何的修改。
JIT是一种动态编译中间代码的方式, LLVM JIT可以将优化后的IR代码最终转化为机器码,开发者可以很方便的使用。
表达式编译执行的实现大部分工作在于前端代码的开发,即如何将SQL中表达式转化为IR代码,对于IR代码的优化及编译生成机器码,LLVM提供了相应的接口可快速使用。
更多LLVM功能及特性大家可以查看LLVM官网。
3 表达式计算框架
表达式计算可支持解释执行及编译执行两种计算方式,解释执行是将输入的数据通过一个通用的后缀计算框架进行执行,不生成任何目标程序。编译执行则先将表达式编译成机器码然后进行执行。
对于一条sql中的表达式,从请求到执行过程如下:
3.1 表达式解释执行
表达式解释执行的实现使用的是后缀计算,将表达式可执行树作为输入,给一个通用的后缀表达式计算框架进行分析,并得出计算结果。
以(c1+c2)*c1+(c1+c2)*3这个表达式为例,表达式树如图(a):
在编译阶段,表达式语义树通过code gen过程生成表达式可执行树,如图(b), 可执行树中每个节点存入不同的item,灰色表示数据item(data_item),黄色表示操作item(operator), data_item中会包含数据类型信息及实际数据等,operator_item会包含操作类型,ExprOperator指针等信息。
在执行阶段,表达式后缀计算使用栈实现,后续遍历表达式可执行树,进行出栈和压栈。对于data_item, 需要大量分支判断数据类型,然后获取数据,并根据不同数据类型选择不同的计算函数。对于opterator_item, 需要通过虚函数调用calc方法,使用不同operater的计算函数,具体继承关系如图(c),最终获得结果。
通用的后缀表达式计算框架为了能够处理不同数据类型及不同的操作,需要用到大量的判断分支及虚函数,这些操作对于cpu cache及cpu实际利用率等均不是很友好,因此对性能会有极大的影响,而编译执行可以很好的解决这些问题。
3.2 表达式编译执行
编译执行在编译期就利用了已有的信息(包括表达式操作的类型(EQAUL,ADD...)、数据的类型(INT, VARCHAR...)),重新“coding”,生成中间代码,使得执行期代码会非常精简,减少在执行过程中为了判断这些类型而增加的各种分支判断、函数调用和虚函数的使用, 整个表达式的执行过程在一个由LLVM动态生成的函数中即可完成。从而可以大大的提升实际CPU使用率, 因此编译执行可以很好的解决解释执行中遇到的问题。
3.2.1 编译执行过程
还是以上面的(c1+c2)*c1+(c1+c2)*3这个表达式为例,假设c1,c2均为BIGINT类型。
在编译阶段,主要有三个步骤:
1.
IR代码生成,通过分析表达式语义树(对数据类型和操作类型均已知),使用LLVM codegen的API生成IR代码如图(a)。
2.
代码优化,我们可以看到,c1+c2表达式做了两次计算,而这个表达式是可以作为公共表达式进行抽取的,经过LLVM 优化后,IR代码如图(b), 只进行了一次c1+c2的计算,总的执行命令也得到减少。另外,使用后缀表达式计算时,所有的中间结果都需要在内存中物化;而编译后的代码可以将中间结果保存在CPU的寄存器中,直接用于下一步的计算,大大的提升了执行效率。LLVM还提供了很多类似的优化,我们都可以直接使用,从而大大提升表达式的计算速度。
3.
即时编译, 通过LLVM中ORC(On-Request Compilation) JIT对优化IR代码进行即时编译,生成可执行机器码,并获取该函数指针。
在执行阶段,通过函数指针调用该函数,即可获得结果。从IR代码可以看出,执行期只需要一次函数调用,大大减少为判断数据类型及操作类型等引起的分支代码及虚函数调用。因此执行效率很高。
3.2.2 优缺点分析
编译执行优点是可以大幅提升执行期的效率,但其缺点是编译时间相对较长,测试发现,对于以上这个简单的表达式, 其编译期耗时在5ms左右,并且随着表达式越复杂,编译期耗时越长。为解决这类问题常用的解决方案是CACHE。
3.3 结果对比
1、对100w行数据分别进行不同表达式计算时,表达式解释执行和编译执行耗时的测试结果对比,从结果可以看出,编译执行的执行效率是解释执行效率的10倍以上。
2、使用TPC-H lineitem表1G数据(约600w行),不同数据库在同样的测试环境执行如下SQL:
SELECT SUM(CASE WHEN l_partkey IN (1,2,3,7)
THEN l_linenumber + l_partkey + 10
ELSE l_linenumber + l_partkey + 5
END) AS result FROM lineitem;
该SQL表达式部分执行耗时测试结果如下:
文章转载自公众号:OceanBase