最近咱们扩展了 TiDB 表达式计算框架,增长了向量化计算接口,初期的性能测试显示,多数表达式计算性能可大幅提高,部分甚至可提高 1~2 个数量级。为了让全部的表达式都能受益,咱们须要为全部内建函数实现向量化计算。git
TiDB 的向量化计算是在经典 Volcano 模型上的进行改进,尽量利用 CPU Cache,SIMD Instructions,Pipeline,Branch Predicatation 等硬件特性提高计算性能,同时下降执行框架的迭代开销,这里提供一些参考文献,供感兴趣的同窗阅读和研究:github
Balancing Vectorized Query Execution with Bandwidth-Optimized Storageapp
The Design and Implementation of Modern Column-Oriented Database Systems框架
在这篇文章中,咱们将描述:ide
如何在计算框架下实现某个函数的向量化计算;函数
如何在测试框架下作正确性和性能测试;性能
如何参与进来成为 TiDB Contributor。测试
在 TiDB 中,数据按列在内存中连续存在 Column 内,Column 详细介绍请看:TiDB 源码阅读系列文章(十)Chunk 和执行框架简介。本文所指的向量,其数据正是存储在 Column 中。ui
咱们把数据类型分为两种:
定长类型:Int64
、Uint64
、Float32
、Float64
、Decimal
、Time
、Duration
;
变长类型:String
、Bytes
、JSON
、Set
、Enum
。
定长类型和变长类型数据在 Column 中有不一样的组织方式,这使得他们有以下的特色:
定长类型的 Column 能够随机读写任意元素;
变长类型的 Column 能够随机读,但更改中间某元素后,可能须要移动该元素后续全部元素,致使随机写性能不好。
对于定长类型(如 int64
),咱们在计算时会将其转成 Golang Slice(如 []int64
),而后直接读写这个 Slice。相比于调用 Column 的接口,须要的 CPU 指令更少,性能更好。同时,转换后的 Slice 仍然引用着 Column 中的内存,修改后不用将数据从 Slice 拷贝到 Column 中,开销降到了最低。
对于变长类型,元素长度不固定,且为了保证元素在内存中连续存放,因此不能直接用 Slice 的方式随机读写。咱们规定变长类型数据以追加写(append
)的方式更新,用 Column 的 Get()
接口进行读取。
总的来讲,变长和定长类型的读写方式以下:
定长类型(以 int64
为例)
a. ResizeInt64s(size, isNull)
:预分配 size 个元素的空间,并把全部位置的 null
标记都设置为 isNull
;
b. Int64s()
:返回一个 []int64
的 Slice,用于直接读写数据;
c. SetNull(rowID, isNull)
:标记第 rowID
行为 isNull
。
变长类型(以 string
为例)
a. ReserveString(size)
:预估 size 个元素的空间,并预先分配内存;
b. AppendString(string)
: 追加一个 string 到向量末尾;
c. AppendNull()
:追加一个 null
到向量末尾;
d. GetString(rowID)
:读取下标为 rowID
的 string 数据。
固然还有些其余的方法如 IsNull(rowID)
,MergeNulls(cols)
等,就交给你们本身去探索了,后面会有这些方法的使用例子。
向量化的计算接口大概以下(完整的定义在这里):
vectorized() bool vecEvalXType(input *Chunk, result *Column) error
XType
可能表示 Int
, String
等,不一样的函数须要实现不一样的接口;
input
表示输入数据,类型为 *Chunk
;
result
用来存放结果数据。
外部执行算子(如 Projection,Selection 等算子),在调用表达式接口进行计算前,会经过 vectorized()
来判断此表达式是否支持向量化计算,若是支持,则调用向量化接口,不然就走行式接口。
对于任意表达式,只有当其中全部函数都支持向量化后,才认为这个表达式是支持向量化的。
好比 (2+6)*3
,只有当 MultiplyInt
和 PlusInt
函数都向量化后,它才能被向量化执行。
要实现函数向量化,还须要为其实现 vecEvalXType()
和 vectorized()
接口。
在 vectorized()
接口中返回 true
,表示该函数已经实现向量化计算;
在 vecEvalXType()
实现此函数的计算逻辑。
还没有向量化的函数在 issue/12058 中,欢迎感兴趣的同窗加入咱们一块儿完成这项宏大的工程。
向量化代码需放到以 _vec.go
结尾的文件中,若是尚未这样的文件,欢迎新建一个,注意在文件头部加上 licence 说明。
这里是一个简单的例子 PR/12012,以 builtinLog10Sig
为例:
这个函数在 expression/builtin_math.go
文件中,则向量化实现需放到文件 expression/builtin_math_vec.go
中;
builtinLog10Sig
原始的非向量化计算接口为 evalReal()
,那么咱们须要为其实现对应的向量化接口为 vecEvalReal()
;
实现完成后请根据后续的说明添加测试。
下面为你们介绍在实现向量化计算过程当中须要注意的问题。
存储表达式计算中间结果的向量可经过表达式内部对象 bufAllocator
的 get()
和 put()
来获取和释放,参考 PR/12014,以 builtinRepeatSig
的向量化实现为例:
buf2, err := b.bufAllocator.get(types.ETInt, n) if err != nil { return err } defer b.bufAllocator.put(buf2) // 注意释放以前申请的内存
如前文所说,咱们须要使用 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]) } }
如前文所说,咱们须要使用 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))) } }
全部受 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 } ... }
咱们作了一个简易的测试框架,可避免你们测试时作一些重复工做。
该测试框架的代码在 expression/bench_test.go
文件中,被实如今 testVectorizedBuiltinFunc
和 benchmarkVectorizedBuiltinFunc
两个函数中。
咱们为每个 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}, }, }
具体来讲,上面结构体中的三个字段分别表示:
该函数的返回值类型;
该函数全部参数的类型;
是否使用自定义的数据生成方法(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 中带上你本地机器的测试结果,让咱们对向量化先后的性能有一个对比便可。
为了推动表达式向量化计算,咱们正式成立 Vectorized Expression Working Group,其具体的目标和制度详见这里。与此对应,咱们在 TiDB Community Slack 中建立了 wg-vec-expr channel 供你们交流讨论,不设门槛,欢迎感兴趣的同窗加入。
如何成为 Contributor:
在此 issue 内选择感兴趣的函数并告诉你们你会完成它;
为该函数实现 vecEvalXType()
和 vectorized()
的方法;
在向量化测试框架内添加对该函数的测试;
运行 make dev
,保证全部 test 都能经过;
发起 Pull Request 并完成 merge 到主分支。
若是贡献突出,可能被提名为 reviewer,reviewer 的介绍请看 这里。
若是你有任何疑问,也欢迎到 wg-vec-expr channel 中提问和讨论。
原文阅读:https://pingcap.com/blog-cn/10mins-become-contributor-of-tidb-20190916/