• JIT 指的是即时编译,即程序在运行过程中即时进行编译,其中可以把编译的中间代码缓存或者优化。相对于静态编译代码,即时编译的代码可以处理延迟绑定并增强安全性。
  • LLVM 就提供了一种在程序运行时编译执行代码的程序框架,它对外提供API,使实现JIT 变得更加简单。

PostgreSQL 社区从2016年就开始对JIT 的实现进行了讨论,详见邮件列表

该邮件中解释了PostgreSQL 需要JIT 技术的原因。因为PostgreSQL 代码中实现的都是通用的逻辑,这就导致在执行过程中可能造成大量不必要的跳转和代码分支执行,继而造成大量不必要的指令执行,造成CPU 的压力。而使用JIT 技术可以将代码扁平化(inline)执行,直接调用对应的函数,而且如果已经知道具体输入,可以直接删除掉很多间接代码的执行。

此外,邮件中也说明了在PostgreSQL 中实现JIT 选择LLVM 的理由,概括起来就是LLVM 成熟度更高,更稳定,license 更友好,支持C 语言。

在PostgreSQL 11 的版本中实现了基于LLVM 的JIT,本文主要是浅析JIT 在PostgreSQL 11 中的使用。

PostgreSQL 中JIT 的实现概述

PostgreSQL 11 中实现的JIT,是把对应的JIT 的提供者封装成了一个外部依赖库。这避免了JIT 对主体代码的侵入,用户可以按需开启/关闭JIT 功能,而且还能通过进一步的抽象支持后期扩展不同的JIT 解决方案(目前使用的是LLVM)。不过这样带来的问题就是各个部分使用JIT 技术编译的代码必须和原来的代码位置分开,这样代码易读性可能有所降低。

作为支持JIT 的第一个正式版本,PostgreSQL 11 只实现了一部分功能,下文将简单讲解下各个功能。

在之前的月报中提出了在数据库实现中LLVM 可优化的点,其中包括:

  • 优化频繁调用的存取层
  • 表达式计算
  • 优化执行器流程

PostgreSQL 11 中基本上也实现了这几方面的优化,但是略有不同,目前包含JIT accelerated operations,inlining,optimization。

表达式计算优化可以针对WHERE 条件,agg 运算等实时将表达式的路径编译为具体的代码执行,在此过程中大量的不必要的调用和跳转会被优化掉。

元组变形优化可以将具体元组转化为其在内存中运行的状态,然后根据元组每列的具体类型和元组中列的个数实时编译为具体的代码执行,在此过程中不必要的代码分支会被优化掉。

表达式和元组操作经常会造成分析型场景下的CPU 性能瓶颈,加速这两方面可以提高PostgreSQL 的分析能力。但是除了这两方面,其他的场景也可以进行JIT 的优化,例如元组排序,COPY 解析/输出以及查询中其他部分等等,这些目前没有实现,社区计划将在后续版本中实现。

PostgreSQL 源码中含有大量通用的代码,执行时会经过很多不必要的函数调用和操作。为了提高执行效率,将通用代码重写或维护两份很明显是不可取的。而JIT 技术带来的好处之一就是执行的时候将代码扁平化,去掉不必要的函数调用和操作。以LLVM 为例,Clang 编译器可以生成LLVM IR(中间表示代码)并优化,这在一定意义上就代表了两份代码。在PostgreSQL 中LLVM IR 使用的是bitcode(二进制格式),对应安装在$pkglibdir/bitcode/postgres/ 中,而对应插件的bitcode 会安装在$pkglibdir/bitcode/[extension]/ 中,其中extension 为插件名。

LLVM 中实现了对产生的中间表示代码的优化,这一定程度上也会提升数据库查询的执行速度。但是该过程本身是有相应的代价的,有些优化可能代价比较低,可以很好地提高性能,而有些可能只有在大的查询中才会体现其提高性能的作用。所以,在PostgreSQL 中定制了一些GUC 参数来限制JIT 功能的开启,详见下文。

与JIT 相关的GUC 参数

在使用JIT 的过程中,有以下几个GUC 参数与之相关,分别是:

  • jit_provider,该参数表示提供JIT 的依赖库,默认为llvmjit。其实目前PostgreSQL 11 也只实现了llvmjit 一种方式。如果填写了不存在的依赖库,JIT 不会生效,也没有error 产生。
  • jit_above_cost,表示超过多少cost 的查询才会使用JIT 功能,其中不包含开销比较大的optimization。因为JIT 会增加一定的开销,所以这个参数可以使得满足要求的查询使用JIT,这样更大概率会起到加速的效果。默认为100000,如果设置为-1 则关闭JIT。
  • jit_inline_above_cost,表示超过多少cost 的查询使用JIT 的inline 功能。默认为500000,-1则关闭inline 功能。如果把这个值设置的比jit_above_cost 小,则达到了该cost,JIT 还是不会触发,没有意义。
  • jit_optimize_above_cost,表示超过多少cost 的查询使用JIT 的optimization 功能。默认为500000,-1则关闭优化功能。和jit_inline_above_cost 一样,如果把这个值设置的比jit_above_cost 小,没有意义。建议该值设置的比jit_inline_above_cost 大,这样可以在触发inline 功能后,开启optimization 功能。

可以看出,因为目前JIT 功能开启所需要的代价没有很好的办法进行建模,也没有很好的方法来估计,所以导致JIT 功能无法作为代价估计模型中一种可量化的代价。目前实现的策略是按照查询的代价来一刀切是否使用JIT 相应功能,还算是比较简单有效。但是,这并不是特别的优雅。很有可能只有某个部分的查询计划更适合使用JIT 功能。不过要想实现查询的某个部分使用JIT 功能需要一些额外的信息输入和判断,这带来的代价是否足够小也是存疑的。

直到这里,我们基本对PostgreSQL 中的JIT 功能有所了解。接下来,我们会讲如何启用JIT,并且以两个例子看下JIT 的效果。

简单的测试

我们针对JIT 做了下两组简单的测试,加深对其的理解。

先来一组社区邮件中给出的经典测试:

可以看出:

  • select sum(c8) from t*; 在JIT 开启下大约有25% 左右的性能提升。
  • select sum(c2), sum(c3), sum(c4), sum(c5), sum(c6), sum(c7), sum(c8) from t*; 在JIT 开启下大约有29% 左右的性能提升。

再来一组简单查询开启JIT 后的测试:

可以看出:

  • 没有达到对应GUC 参数规定的cost,即使jit=on,JIT 也不会起作用。
  • JIT 会继承并行查询带来的性能提升。

JIT 技术对数据库操作系统来说是提高AP 能力的有效手段,但是在工程化的道路上要考虑很多实现的问题。PostgreSQL 社区目前是采用的外部依赖按需加载的方式,将JIT 作为一种外挂手段在一定场景下提高了性能,但是由于没有有效手段评估JIT 开启的代价,需要经验和具体业务场景的测试来判断JIT 功能开启是否能够提高性能。另外,目前实现的JIT 功能相对来说比较单一,只是初期版本,尚未成熟,还需要很长的开发周期来稳定和迭代。

参考文献

[1] PgSQL · 特性分析· JIT 在数据仓库中的应用价值

[2]