最近咱們擴展了 TiDB 表達式計算框架,增長了向量化計算接口,初期的性能測試顯示,多數表達式計算性能可大幅提高,部分甚至可提高 1~2 個數量級。爲了讓全部的表達式都能受益,咱們須要爲全部內建函數實現向量化計算。git
TiDB 的向量化計算是在經典 Volcano 模型上的進行改進,儘量利用 CPU Cache,SIMD Instructions,Pipeline,Branch Predicatation 等硬件特性提高計算性能,同時下降執行框架的迭代開銷,這裏提供一些參考文獻,供感興趣的同窗閱讀和研究:github
Balancing Vectorized Query Execution with Bandwidth-Optimized Storagebash
The Design and Implementation of Modern Column-Oriented Database Systemsapp
在這篇文章中,咱們將描述:框架
如何在計算框架下實現某個函數的向量化計算;ide
如何在測試框架下作正確性和性能測試;函數
如何參與進來成爲 TiDB Contributor。性能
在 TiDB 中,數據按列在內存中連續存在 Column 內,Column 詳細介紹請看:TiDB 源碼閱讀系列文章(十)Chunk 和執行框架簡介。本文所指的向量,其數據正是存儲在 Column 中。測試
咱們把數據類型分爲兩種:
定長類型: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 中提問和討論。