专栏首页PingCAP的专栏十分钟成为 Contributor 系列 | 助力 TiDB 表达式计算性能提升 10 倍
原创

十分钟成为 Contributor 系列 | 助力 TiDB 表达式计算性能提升 10 倍

最近我们扩展了 TiDB 表达式计算框架,增加了向量化计算接口,初期的性能测试显示,多数表达式计算性能可大幅提升,部分甚至可提升 1~2 个数量级。为了让所有的表达式都能受益,我们需要为所有内建函数实现向量化计算。

TiDB 的向量化计算是在经典 Volcano 模型上的进行改进,尽可能利用 CPU Cache,SIMD Instructions,Pipeline,Branch Predicatation 等硬件特性提升计算性能,同时降低执行框架的迭代开销,这里提供一些参考文献,供感兴趣的同学阅读和研究:

  1. MonetDB/X100: Hyper-Pipelining Query Execution
  2. Balancing Vectorized Query Execution with Bandwidth-Optimized Storage
  3. The Design and Implementation of Modern Column-Oriented Database Systems

在这篇文章中,我们将描述:

  1. 如何在计算框架下实现某个函数的向量化计算;
  2. 如何在测试框架下做正确性和性能测试;
  3. 如何参与进来成为 TiDB Contributor。

表达式向量化

1. 如何访问和修改一个向量

在 TiDB 中,数据按列在内存中连续存在 Column 内,Column 详细介绍请看:TiDB 源码阅读系列文章(十)Chunk 和执行框架简介。本文所指的向量,其数据正是存储在 Column 中。

我们把数据类型分为两种:

  1. 定长类型:Int64Uint64Float32Float64DecimalTimeDuration
  2. 变长类型:StringBytesJSONSetEnum

定长类型和变长类型数据在 Column 中有不同的组织方式,这使得他们有如下的特点:

  1. 定长类型的 Column 可以随机读写任意元素;
  2. 变长类型的 Column 可以随机读,但更改中间某元素后,可能需要移动该元素后续所有元素,导致随机写性能很差。

对于定长类型(如 int64),我们在计算时会将其转成 Golang Slice(如 []int64),然后直接读写这个 Slice。相比于调用 Column 的接口,需要的 CPU 指令更少,性能更好。同时,转换后的 Slice 仍然引用着 Column 中的内存,修改后不用将数据从 Slice 拷贝到 Column 中,开销降到了最低。

对于变长类型,元素长度不固定,且为了保证元素在内存中连续存放,所以不能直接用 Slice 的方式随机读写。我们规定变长类型数据以追加写(append)的方式更新,用 Column 的 Get() 接口进行读取。

总的来说,变长和定长类型的读写方式如下:

  1. 定长类型(以 int64 为例)
a. `ResizeInt64s(size, isNull)`:预分配 size 个元素的空间,并把所有位置的 `null` 标记都设置为 `isNull`;
b.  `Int64s()`:返回一个 `[]int64` 的 Slice,用于直接读写数据;
c.  `SetNull(rowID, isNull)`:标记第 `rowID` 行为 `isNull`。
  1. 变长类型(以 string 为例)
a. `ReserveString(size)`:预估 size 个元素的空间,并预先分配内存;
b. `AppendString(string)`: 追加一个 string 到向量末尾;
c.  `AppendNull()`:追加一个 `null` 到向量末尾;
d.  `GetString(rowID)`:读取下标为 `rowID` 的 string 数据。

当然还有些其他的方法如 IsNull(rowID)MergeNulls(cols) 等,就交给大家自己去探索了,后面会有这些方法的使用例子。

2. 表达式向量化计算框架

向量化的计算接口大概如下(完整的定义在这里):

vectorized() bool
vecEvalXType(input *Chunk, result *Column) error
  • XType 可能表示 Int, String 等,不同的函数需要实现不同的接口;
  • input 表示输入数据,类型为 *Chunk
  • result 用来存放结果数据。

外部执行算子(如 Projection,Selection 等算子),在调用表达式接口进行计算前,会通过 vectorized() 来判断此表达式是否支持向量化计算,如果支持,则调用向量化接口,否则就走行式接口。

对于任意表达式,只有当其中所有函数都支持向量化后,才认为这个表达式是支持向量化的。

比如 (2+6)*3,只有当 MultiplyIntPlusInt 函数都向量化后,它才能被向量化执行。

为函数实现向量化接口

要实现函数向量化,还需要为其实现 vecEvalXType()vectorized() 接口。

  • vectorized() 接口中返回 true ,表示该函数已经实现向量化计算;
  • vecEvalXType() 实现此函数的计算逻辑。

尚未向量化的函数在 issue/12058 中,欢迎感兴趣的同学加入我们一起完成这项宏大的工程。

向量化代码需放到以 _vec.go 结尾的文件中,如果还没有这样的文件,欢迎新建一个,注意在文件头部加上 licence 说明。

这里是一个简单的例子 PR/12012,以 builtinLog10Sig 为例:

  1. 这个函数在 expression/builtin_math.go 文件中,则向量化实现需放到文件 expression/builtin_math_vec.go 中;
  2. builtinLog10Sig 原始的非向量化计算接口为 evalReal(),那么我们需要为其实现对应的向量化接口为 vecEvalReal()
  3. 实现完成后请根据后续的说明添加测试。

下面为大家介绍在实现向量化计算过程中需要注意的问题。

1. 如何获取和释放中间结果向量

存储表达式计算中间结果的向量可通过表达式内部对象 bufAllocatorget()put() 来获取和释放,参考 PR/12014,以 builtinRepeatSig 的向量化实现为例:

buf2, err := b.bufAllocator.get(types.ETInt, n)
if err != nil {
    return err
}
defer b.bufAllocator.put(buf2) // 注意释放之前申请的内存

2. 如何更新定长类型的结果

如前文所说,我们需要使用 ResizeXType()XTypes() 来初始化和获取用于存储定长类型数据的 Golang Slice,直接读写这个 Slice 来完成数据操作,另外也可以使用 SetNull() 来设置某个元素为 NULL。代码参考 PR/12012,以 builtinLog10Sig 的向量化实现为例:

f64s := result.Float64s()
for i := 0; i < n; i++ {
    if isNull {
        result.SetNull(i, true)
    } else {
        f64s[i] = math.Log10(f64s[i])
    }
}

3. 如何更新变长类型的结果

如前文所说,我们需要使用 ReserveXType() 来为变长类型预分配一段内存(降低 Golang runtime.growslice() 的开销),使用 AppendXType() 来追加一个变长类型的元素,使用 GetXType() 来读取一个变长类型的元素。代码参考 PR/12014,以 builtinRepeatSig 的向量化实现为例:

result.ReserveString(n)
...
for i := 0; i < n; i++ {
    str := buf.GetString(i)
    if isNull {
        result.AppendNull()
    } else {
    result.AppendString(strings.Repeat(str, int(num)))
    }
}

4. 如何处理 Error

所有受 SQL Mode 控制的 Error,都利用对应的错误处理函数在函数内就地处理。部分 Error 可能会被转换成 Warn 而不需要立即抛出。

这个比较杂,需要查看对应的非向量化接口了解具体行为。代码参考 PR/12042,以 builtinCastIntAsDurationSig 的向量化实现为例:

for i := 0; i < n; i++ {
    ...
    dur, err := types.NumberToDuration(i64s[i], int8(b.tp.Decimal))
    if err != nil {
       if types.ErrOverflow.Equal(err) {
          err = b.ctx.GetSessionVars().StmtCtx.HandleOverflow(err, err) // 就地利用对应处理函数处理错误
       }
       if err != nil { // 如果处理不掉就抛出
          return err
       }
       result.SetNull(i, true)
       continue
    }
    ...
}

5. 如何添加测试

我们做了一个简易的测试框架,可避免大家测试时做一些重复工作。

该测试框架的代码在 expression/bench_test.go 文件中,被实现在 testVectorizedBuiltinFuncbenchmarkVectorizedBuiltinFunc 两个函数中。

我们为每一个 builtin_XX_vec.go 文件增加了 builtin_XX_vec_test.go 测试文件。当我们为一个函数实现向量化后,需要在对应测试文件内的 vecBuiltinXXCases 变量中,增加一个或多个测试 case。下面我们为 log10 添加一个测试 case:

var vecBuiltinMathCases = map[string][]vecExprBenchCase {
    ast.Log10: {
        {types.ETReal, []types.EvalType{types.ETReal}, nil},
    },
}

具体来说,上面结构体中的三个字段分别表示:

  1. 该函数的返回值类型;
  2. 该函数所有参数的类型;
  3. 是否使用自定义的数据生成方法(dataGener),nil 表示使用默认的随机生成方法。

对于某些复杂的函数,你可自己实现 dataGener 来生成数据。目前我们已经实现了几个简单的 dataGener,代码在 expression/bench_test.go 中,可直接使用。

添加好 case 后,在 expression 目录下运行测试指令:

# 功能测试
GO111MODULE=on go test -check.f TestVectorizedBuiltinMathFunc

# 性能测试
go test -v -benchmem -bench=BenchmarkVectorizedBuiltinMathFunc -run=BenchmarkVectorizedBuiltinMathFunc

在你的 PR Description 中,请把性能测试结果附上。不同配置的机器,性能测试结果可能不同,我们对机器配置无任何要求,你只需在 PR 中带上你本地机器的测试结果,让我们对向量化前后的性能有一个对比即可。

如何成为 Contributor

为了推进表达式向量化计算,我们正式成立 Vectorized Expression Working Group,其具体的目标和制度详见这里。与此对应,我们在 TiDB Community Slack 中创建了 wg-vec-expr channel 供大家交流讨论,不设门槛,欢迎感兴趣的同学加入。

如何成为 Contributor:

  1. 在此 issue 内选择感兴趣的函数并告诉大家你会完成它;
  2. 为该函数实现 vecEvalXType()vectorized() 的方法;
  3. 在向量化测试框架内添加对该函数的测试;
  4. 运行 make dev,保证所有 test 都能通过;
  5. 发起 Pull Request 并完成 merge 到主分支。

如果贡献突出,可能被提名为 reviewer,reviewer 的介绍请看 这里

如果你有任何疑问,也欢迎到 wg-vec-expr channel 中提问和讨论。

原文阅读:https://pingcap.com/blog-cn/10mins-become-contributor-of-tidb-20190916/

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 十分钟成为 Contributor 系列 | 支持 AST 还原为 SQL

    SQL 语句发送到 TiDB 后首先会经过 parser,从文本 parse 成为 AST(抽象语法树),AST 节点与 SQL 文本结构是一一对应的,我们通过...

    PingCAP
  • 十分钟成为 Contributor 系列:重构内建函数进度报告

    为了方便社区同学更好地参与 TiDB 项目,本文一方面对继上一篇文章发布后参考社区的反馈对表达式计算框架所做的修改进行详细介绍,另一方面对尚未重写的 built...

    PingCAP
  • TiDB 源码阅读系列文章(四)Insert 语句概览

    本文为 TiDB 源码阅读系列文章的第四篇。上一篇文章简单介绍了整体流程,无论什么语句,大体上是在这个框架下运行,DDL 语句也不例外。

    PingCAP
  • 用友战略 3.0 是自我救赎,还是里程杯式的突破?

    作者:T客汇 人称T客 关键词:用友,战略3.0,金融,互联网化 网址:www.tikehui.com 近来,用友发布了战略 3.0,对于用友而言这是里程杯式...

    人称T客
  • 哪些传统软件厂商会成为中国未来IT业的潜力股—用友篇

    又快到了一年的收获的季节,很多IT公司都开始盘点这一年来的收成,最近也总有一些网友让我评价一下IT公司的股市表现,说实话T哥从不炒股,对股票一点研究没有,我觉得...

    人称T客
  • 用友:ERP已成过去时 平台+生态+聚合服务成未来

    用友网络执行总裁陈强兵表示:ERP已成过去时,未来用友会将重心放在平台、生态和少量的核心云服务,其他的服务通过生态上的伙伴聚合来完成。在刚刚结束的伙伴大会上用友...

    人称T客
  • 用友2017年第三财季亏损2.6亿 增长之路缘何如此艰难?

    前两天有人私信让我评价一下用友刚刚公布的最新财报,说实话关注了用友这么多年,用友一直没有多大变化,从2012年到2016年用友就进入了一个增长瓶颈期,营收一直在...

    人称T客
  • 盘点用友网络的2015

    今年整个ERP市场又一句来评价已是外强中干,看似繁花似锦实际上已是残枝败柳,可能很多人不会认同我的观点,其实这没有什么?连国外的知名厂商SAP和Oracle也同...

    人称T客
  • 用友Q1亏损加剧 转型良方何在?

    其实关于用友的转型近年来一直是大家讨论的焦点,无论是风风火火的互联网金融,还是去年中旬成立的用友超客,以及担负用友云战略先锋的畅捷通,一直都备受争议,业内普遍认...

    人称T客
  • 管理软件厂商集体患上了互联网焦虑症-用友篇

    关于互联网焦虑症不止是IT行业,其实整个行业都存在,只不过站在最前沿的IT厂商反应明显,今天我们讲讲用友的焦虑何在?节后我们一起来分析《SAP和Oracle在焦...

    人称T客

扫码关注云+社区

领取腾讯云代金券