tidb:向量化执行使表达式性能提升10倍成为可能

yzs的专栏 yzs的专栏     2022-12-03     364

关键词:


向量化执行使表达式性能提升10倍成为可能

查询执行引擎对数据库系统性能非常重要。TIDB是一个开源兼容MySQL的HTAP数据库,部署广泛使用的火山模型来执行查询。不幸的是,当查询一个大库时,向量化模型会造成较高的解释开销以及较低的CPU CACHE命中率。

受MonetDB/X100: Hyper-Pipelining Query Execution论文启发,在TIDB中部署向量化执行引擎以提高查询性能。(建议学习 Andy Pavlo 的Query Execution课程,其中详细介绍了有关执行模型和表达式计算的原理)。

2017年年末,我们对TIDB的SQL执行引擎完成了3项优化:

1) 在执行引擎中完成列式存储。类似Apache的Arrow。参考pull request (PR) #4856:https://github.com/pingcap/tidb/pull/4856。

2) 将一次一个tuple的迭代模式(火山模型)改成一次一批(1024个tuple),参考:https://github.com/pingcap/tidb/pull/5178

3) 基于向量化执行原则,优化各种查询操作符:https://github.com/pingcap/tidb/pull/5184

得益于这些优化,与TIDB1.0相比,TIDB2.0显著提升了查询分析的性能。可以查看TPC-H性能:

​https://github.com/pingcap/docs/blob/release-2.1/benchmark/v2.1-performance-benchmarking-with-tpch.md​

最近我们release了TiDB2.1和3.0,向量化执行引擎更加稳定。现在正在开发TiDB4.0,包括向量化表达式。

本文,深入分析了为什么使用向量化引擎,如何实现它以及如何与社区贡献者合作完成多于360个函数的向量化,还有对未来的看法。

为什么使用向量化

之前TiDB实现了火山模型的执行引擎。这个迭代模型使用标准数据访问接口。在各个算子之间执行open()-next()-close(),一行一行处理数据。火山模型简单且可扩展。

但是,当执行大型查询时,火山模型会带来较高的解析代价。另外,也不能重复你利用现代CPU硬件的特性,如CPU CACHE、分支预测、指令流水线。

向量化执行使用单指令在内存中执行一组连续的相似的数据项。与火山模型相比,向量化模型大大降低了解释开销。因此我们选择了向量化执行引擎。

本节中,使用表达式colA*0.8 + colB来展示基于行的执行和基于向量化执行之间的开销差距。

TIDB根据算术运算符和运算符优先级,将此表达式解析为表达式求值树。在这个树中,每个非叶子节点代表一个算术运算符,叶节点代表数据源。每个非叶节点要么是一个常量如0.8,要么是表中的一个字段如colA。节点之间的父子关系表示计算上的依赖关系:子节点的结算结果是父节点的输入数据。

TiDB:向量化执行使表达式性能提升10倍成为可能_python

 表达式求值树

每个节点的计算逻辑可以抽象成下面接口:

type Node interface 
eval(row Row) (val float64, isNull bool)

以*,0.8,col节点为例,3个节点都实现了上面的接口,伪代码如下:

func (node *multiplyRealNode) eval(row Row) (float64, bool) 
v1, null1 := node.leftChild.eval(row)
v2, null2 := node.rightChild.eval(row)
return v1 * v2, null1 || null2



func (node *constantNode) eval(row Row) (float64, bool)
return node.someConstantValue, node.isNull // 0.8 in this case
func (node *columnNode) eval(row Row) (float64, bool) 
return row.GetReal(node.colIdx)

基于行执行的解释开销

和火山模型类似,上面讨论的表达式实现是对行进行迭代。

逐行迭代的解释开销是多少?下面我们来看一个函数在TiDB中的实现。

builtinArithmeticMultiplyRealSig函数实现2个浮点数相乘。下面的代码块描述了这个函数的实现。右边的数组表示对应行的汇编指令数,是代码汇编后得到的。要注意,此块仅包含在正常条件下迭代的行,并忽略错误处理的逻辑:

TiDB:向量化执行使表达式性能提升10倍成为可能_数据库_02

下面列出了builtinArithmeticMultiplyRealSig每个功能任务及执行它的汇编指令数量。

TiDB:向量化执行使表达式性能提升10倍成为可能_机器学习_03

每次这个函数执行乘法时,82条指令中仅有8条在执行“真正的”乘法,这仅占总指令的10%左右,其他90%被视为解释开销。一旦将这个函数向量化,它的性能提高了仅9倍。参见:https://github.com/pingcap/tidb/pull/12543

向量化减少解释开销

和SQL算子的向量化优化一样,我们可以一次处理并返回一批数据。这减少了表达式计算过程中的解释开销。

假设一批数据有1024行,优化后每次调用一个函数处理这1024行数据,然后返回。函数调用的解释开销变成原来的1/1024.

因此,我们可以添加以下接口来向量化表达式计算:

type Node interface

  batchEvalReal(rows []Row) (vals []float64, isNull []bool)

我们的向量化处理接口长什么样?为什么?

真正的源代码和上面显示的模型并不完全一样。它看起来像这样:

type VecNode interface

  vecEvalReal(input *Chunk, result *Column)

想知道为什么?

为了说明原因,先介绍下TiDB的chunk结构。也就是查询执行阶段数据在内存中的表示。

2017年底,在进行向量化优化,引入了chunk概念。一个chunk由多列组成。列由两种类型:

1) 固定长度的列,数据时固定长度,不能改变

2) 变长列,数据长度可变

TiDB:向量化执行使表达式性能提升10倍成为可能_大数据_04

不管数据长度是固定的还是变长的,列中数据在Column.data字段(即数组)中是连续存储在内存中的。如果数据长度变化,column.offset记录数据偏移量。如果数据是固定长度,则不需要记录偏移量。

下图说明了我们最近为chunk引入的新向量访问接口:

TiDB:向量化执行使表达式性能提升10倍成为可能_机器学习_05

1) 对于定长数据,如int64,Golang的unsafe包直接转换column.data为[]int64([]int64中的Int64s()),返回结果。读取或更改column.data的用户可以直接操作这个数组。这是访问固定长度数据的最有效方式。

2) 对于变长数据,例如字符串,可以使用GetString(rowIdx int) string来获取对应行的数据,仅能以追加的方式更新。随机更改变长数据列中一个元素,需要移动后续所有数据。这会带来巨大开销。为了提升性能,此操作未在column中。

再返回来看下接口设计。

基于chunk的实现以及Golang的特性,我们优化了表达式计算以下几个方面:

1) 直接通过*chunk而不是[]Row,避免创建大量Row对象。降低了Golang垃圾回收的压力并提高了性能

2) column.data通过column而不是Row,减少函数调用次数。帮助减少解释开销和加速访问数据。

3) 将用于存储数据的列放到参数中并传递,而不是直接返回[]float64和[]bool数组。提高了内存利用率,减少了Golang GC开销。

基于这些优化,我们的向量化表达式计算接口变成了今天这个样子。

怎么实现向量化执行引擎?

本节介绍如何实现向量化。

以multiplyRealNode为例:

TiDB:向量化执行使表达式性能提升10倍成为可能_机器学习_06

对于这个功能:

1) 前2行:cache pool中的ColumnBuffer用于缓冲右孩子(第二个参数)的数据。左孩子的数据存储再result指针指向的内存中。

2) Columns.MergeNulls(cols...)合并多列的NULL标签。这个方法类似result.nulls[i] = result.nulls[i] || buf.nulls[i]。Column内部使用一个bitmap来维护NULL标签。当调用这个函数时,一个列来做一个按位操作来核并NULLs。

3) 一个循环直接将左右字节的的数据相乘。

4) 再乘法过程中中,该函数调用左右子接口来获取他们的数据。

这种实现方式通过向量化减少了解释开销,对现代CPU更有利:

1) 顺序访问一个向量数据,减少了CPU CACHE的miss

2) 大多数计算工作在一个简单循环中,有助于CPU分支预测与指令流水线。

向量化执行和基于行的执行引擎性能比较

本节使用TiDB源码进行基准测试,并比较向量化前后的性能。使用相同数据(2列浮点数组成的1024行)分别col0*0.8 + col1计算: 

TiDB:向量化执行使表达式性能提升10倍成为可能_python_07

上面的结果表明向量化执行比基于行的执行引擎快4倍。下图对比了LT向量化前后各种小于(LT)函数的性能。横轴表示LT用于测试的函数,纵轴表示完成操作持续的时间(单位纳秒)

TiDB:向量化执行使表达式性能提升10倍成为可能_java_08

 

下面比较了向量化前后算术函数的性能:

TiDB:向量化执行使表达式性能提升10倍成为可能_机器学习_09

我们测试了300多个向量化函数后,发现这些函数中有超过50%的函数性能提高了1倍以上,18.7%的函数实现了10的性能提升。

TiDB:向量化执行使表达式性能提升10倍成为可能_数据库_10

如何将360+个内置函数向量化?

迈向4.0的过程中,表达式向量化是一个巨大的工程。因为涉及500多个内置函数。开发人员相当少-完成相当困难。位更有效开发代码,使用Golang将尽可能多的内置函数向量化test/template。此模板生成向量化函数的源码。位让更多人参与这个项目,我们在开发者社区中成立了向量化表达式工作组。通过这种方法,社区贡献者承担了向量化过程中大部分工作。

在我们的努力下,重构了仅三分之二的内置函数,大部分都有显著的性能提升。有些甚至能有一两个数量级的性能提升。

使用一个模板进行向量化

当我们对内置函数进行向量化时,我们发现很多函数都有相似之处。例如,大多数LT( <)、GT( >) 和LE( <=) 函数具有相似的逻辑。它们仅使用的运算符不同。因此,可以使用模板来生成这些函数的代码。目前,Golang 不支持泛型类型和宏定义,所以我们使用text/template包来生成代码。基于Golang模板的语法,我们将要生成的函数抽象成模板。例如,这里是比较函数的模板,如LTand GT:

TiDB:向量化执行使表达式性能提升10倍成为可能_java_11

针对不同类型数据和算子,模板生成相应代码。这个模板在expression/generator包里。

社区帮助完成向量化

除了使用模板向量化内置函数外,还在社区开始了向量化活动,让更多人参与进来。编写了一个脚本,为所有内置函数生成向量化接口:

func (b *builtinCastStringAsIntSig) vecEvalInt(input *chunk.Chunk, result *chunk.Column) error

  return errors.Errorf("not implemented")

func (b *builtinCastStringAsDurationSig) vectorized() bool

  return false

保留函数接口可以避免合并代码时不同pull request同时修改同一个文档引起的冲突。我们还编写了一个测试框架。贡献者将函数向量化后,他们可以使用框架来测试内置函数的正确性和性能,只需编写几行简单的配置即可。

如下代码所示,贡献者只需要在vecBuiltinArithemeticCases 

TiDB:向量化执行使表达式性能提升10倍成为可能_java_12

接下来,它们可以按照我们编写的两个函数运行。然后,他们可以执行正确性和性能测试。

TiDB:向量化执行使表达式性能提升10倍成为可能_数据库_13

正确性和性能测试都直接生成随机数据,并比较向量化执行和基于行执行的性能。上述两个操作可以帮助贡献者轻松向量化我们的内置函数。在社区的帮助下,我们在短短两个月内对 360 多个函数进行了向量化处理。在此期间,我们的社区贡献者提交了 256 份 PR。该活动还产生了九名活跃贡献者(他们在一年内合并了至少 8 个 PR)和两个审阅者(他们在一年内合并了至少 30 个 PR)。

下一步做什么

通过引入向量化执行,我们显着提高了表达式执行的性能。目前,默认情况下,我们的主分支上启用了向量化执行。一旦表达式的所有内置函数都支持向量化执行,该表达式将使用向量化执行。未来我们也会使用向量化执行来提升 TiDB 的 HTAP 能力。

请注意,上面的所有性能测试数据都在内存中。如果数据存储在磁盘上,读取它可能会导致高开销。在这种情况下,向量化执行的好处可能不太明显。但总的来说,向量化执行明显提升了 TiDB 表达式评估的性能。

此外,当我们对表达式进行向量化时,我们发现向量化执行可以应用于许多其他情况以提高性能。例如:

在哈希连接中,我们为内部数据(参见PR #12076)和外部数据(参见PR #12669)向量计算哈希键。在某些场景下,性能分别提升7%和18%。我们要感谢一位名叫sduzh的社区贡献者,他独自完成了这些改进。

在哈希聚合中,我们对数据进行了向量化编码。在本地测试中,性能比以前快了20%到60%。有关详细信息,请参阅PR #12729。

在StreamAggregation运算符中,我们将数据向量划分为组。在本地测试中,性能提升了约 60%。有关详细信息,请参阅PR #12903。

原文

​https://en.pingcap.com/blog/10x-performance-improvement-for-expression-evaluation-made-possible-by-vectorized-execution​

7-10倍写入性能提升:剖析wiredtiger数据页无锁及压缩黑科技

7-10倍写入性能提升:剖析WiredTiger数据页无锁及压缩黑科技导语:计算机硬件在飞速发展,数据规模在急速膨胀,但是数据库仍然使用是十年以前的架构体系,WiredTiger尝试打破这一切,充分利用多核与大内存时代来重新设计数据... 查看详情

国内重要的go语言项目:tidb3.0ga,稳定性和性能大幅提升

...Sysbench性能提升约1.5倍,OLAP方面,TPC-H50GQ15因实现View可以执行,至此TPC-H22个Query均可正常运行。新功能方面增加了窗口函数、视图(实验特性)、分区表、插件系统、悲观锁(实验特性)。截止本文发稿时TiDB已在500+用户的生产... 查看详情

重磅官宣:nacos2.0性能提升10倍

简介:​Nacos2.0作为一个跨代版本,彻底解决了Nacos1.X的性能问题,将性能提升了10倍。作者:席翁继Nacos1.0发布以来,Nacos迅速被成千上万家企业采用,并构建起强大的生态。但是随着用户深入使用,逐渐暴露一些性能问题,因... 查看详情

alijdk8.1.1的优化使ssl性能提升2倍以上

简单说如果你的java容器提供https服务的,性能可以提升两倍以上,这是一个非常非常非常非常非常值得升级的提升。在jdk8.0时摸高压测qps到3000时再也上不去,主要是ssl里面的一个锁效率低,优化后qps达到 8000也很... 查看详情

如何把go调用c的性能提升10倍?

...对C的函数的调用,必须先把当前的goroutine挂起,并切换执行栈到当前的线程M的主栈(大小2MB)。如果不做这个 查看详情

10倍,boostkit鲲鹏全局缓存3大创新技术助力ceph性能提升

...的痛点,采用三大创新技术,有效的提高了Ceph的性能,最高可以将Ceph性能提升10倍。本文分享自华为云社区《【云驻共创】BoostKit鲲鹏全局缓存技术助力Ceph性能提升10倍,真香》,作者: 查看详情

13倍性能,3倍稳定性提升!ucloud云硬盘做了这些事

近期,我们推出高性能SSD云盘,满足用户对高性能的场景需求。SSD云盘相比普通云盘,IOPS提升了13倍,稳定性提升了3倍,平均时延降低了10倍。为了做到这些,我们从去年10月份开始对云盘的架构进行了重新设计,充分减少时延... 查看详情

java中的5个代码性能提升技巧,最高提升近10倍(代码片段)

...已经收录,欢迎Star。这篇文章介绍几个Java开发中可以进行性能优化的小技巧,虽然大多数情况下极致优化代码是没有必要的,但是作为一名技术开发者,我们还是想追求代码的更小、更快,更强。如果哪天你发现... 查看详情

java中的5个代码性能提升技巧,最高提升近10倍(代码片段)

这篇文章介绍几个Java开发中可以进行性能优化的小技巧,虽然大多数情况下极致优化代码是没有必要的,但是作为一名技术开发者,我们还是想追求代码的更小、更快,更强。如果哪天你发现程序的运行速度不尽... 查看详情

java中的5个代码性能提升技巧,最高提升近10倍(代码片段)

...已经收录,欢迎Star。这篇文章介绍几个Java开发中可以进行性能优化的小技巧,虽然大多数情况下极致优化代码是没有必要的,但是作为一名技术开发者,我们还是想追求代码的更小、更快,更强。如果哪天你发现... 查看详情

10个步骤让你的应用提升10倍性能

点击上方“朱小厮的博客”,选择“设为星标”后台回复"书",获取后台回复“k8s”,可领取k8s资料-   目录   - 建议一:使用反向代理服务器让应用更快更安全建议二:增加负载均衡服务器建议三&#x... 查看详情

通过用向量化替换 lambda x 来提高排名函数的性能

】通过用向量化替换lambdax来提高排名函数的性能【英文标题】:Performanceenhancementofrankingfunctionbyreplacementoflambdaxwithvectorization【发布时间】:2017-10-1706:36:21【问题描述】:我有一个排名功能,我将其应用于数百万行的大量列,这... 查看详情

var值计算性能千倍提升——某头部外资银行实例分享

...秒降至10毫秒以下;整体报表查询速度提升近5倍。​打破性能瓶颈,释放业务潜力IRSVaR计算:从40分钟至3.5秒VaR值是银行风险控制中最基础的风险度量指标之一,可以用来度量在一定时间区间内,某个投资组合或资产价值可能出... 查看详情

meta公司内部项目-raptorx:将presto性能提升10倍(代码片段)

...文件描述符和footer缓存Alluxio数据缓存软亲和调度Performance性能UserGuide用户指南概要速览RaptorX是Meta(前“Facebook公司”,下文统称“Meta”)公司的一个内部项目名称&# 查看详情

tidb5.4发版丨新功能解读(代码片段)

...开山之作,包含了许多有用有益的新功能和持续性的性能/稳定性提升。本文着重介绍重要新增功能和特性所带给用户的新体验和价值,按照以下章节来组织:基础性能优化和提升面向云环境的功能拓展产品易用性和... 查看详情

springboot+@async注解一起用,速度提升100倍!

...应的是“同步调用”,同步调用指程序按照定义顺序依次执行,每一行程序都必须等待上一行程序执行完成之后才能执行;异步调用指程序在顺序执行时,不等待异步调用的语句返回结果就执行后面的程序。异步调用几乎是处理... 查看详情

如何利用缓存机制实现java类反射性能提升30倍

1SSM框架简介SSM框架,即SpringMVC+Spring+Mybatis三个开源框架整合在一起的缩写。在SSM框架之前生产环境中SSH框架占据多数,即Struts2+Spring+Hibernate三个开源框架整合而成。后因Struts2爆出众多高危漏洞,导致目前SSM逐渐代替SSH成为主流... 查看详情

我只改五行代码,接口性能提升了10倍!(代码片段)

背景某公司的一个ToB系统,因为客户使用的也不多,没啥并发要求,就一直没有经过压测。这两天来了一个“大客户”,对并发量提出了要求:核心接口与几个重点使用场景单节点吞吐量要满足最低500/s的要求... 查看详情