十分鐘成爲 Contributor 系列 | 助力 TiDB 表達式計算性能提高 10 倍

最近咱們擴展了 TiDB 表達式計算框架,增長了向量化計算接口,初期的性能測試顯示,多數表達式計算性能可大幅提高,部分甚至可提高 1~2 個數量級。爲了讓全部的表達式都能受益,咱們須要爲全部內建函數實現向量化計算。git

TiDB 的向量化計算是在經典 Volcano 模型上的進行改進,儘量利用 CPU Cache,SIMD Instructions,Pipeline,Branch Predicatation 等硬件特性提高計算性能,同時下降執行框架的迭代開銷,這裏提供一些參考文獻,供感興趣的同窗閱讀和研究:github

  1. MonetDB/X100: Hyper-Pipelining Query Executionexpress

  2. Balancing Vectorized Query Execution with Bandwidth-Optimized Storagebash

  3. The Design and Implementation of Modern Column-Oriented Database Systemsapp

在這篇文章中,咱們將描述:框架

  1. 如何在計算框架下實現某個函數的向量化計算;ide

  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

  2. 變長類型(以 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 中提問和討論。

原文閱讀:pingcap.com/blog-cn/10m…

相關文章
相關標籤/搜索