1. 程式人生 > 其它 >Branch 向量化

Branch 向量化

Branch 向量化

問題發現定位

昨天晚上小夥伴告訴我有一個case的效能不太理想,讓我看看

這個查詢長這樣:

SELECT SUM(CASE WHEN LO_SUPPLYCOST + 10000 > 100000 then 1 else 0 END) FROM lineorder_flat;

lineorder_flat 這個表是標準的SSB測試資料集的寬表

看起來很簡單的一個查詢。並行度調整為1跑跑看

+-------------------------------------------------------------------+
| sum(CASE WHEN `LO_SUPPLYCOST` + 10000 > 100000 THEN 1 ELSE 0 END) |
+-------------------------------------------------------------------+
|                                                         299718458 |
+-------------------------------------------------------------------+
1 row in set (10.33 sec)

??? 咋回事,是因為作業系統page cache嗎,再試試

mysql> SELECT SUM(CASE WHEN LO_SUPPLYCOST + 10000 > 100000 then 1 else 0 END) FROM lineorder_flat;
+-------------------------------------------------------------------+
| sum(CASE WHEN `LO_SUPPLYCOST` + 10000 > 100000 THEN 1 ELSE 0 END) |
+-------------------------------------------------------------------+
|                                                         299718458 |
+-------------------------------------------------------------------+
1 row in set (10.45 sec)

好傢伙,還真就這麼慢。

顯然這個case很有問題,先看一下Profile:

          PROJECT_NODE (id=1):(Active: 10s267ms[10267379182ns], % non-child: 96.18%)
             - CommonSubExprComputeTime: 2.752ms
             - ExprComputeTime: 9s922ms
             - PeakMemoryUsage: 0.00 
             - RowsReturned: 600.037902M (600037902)
             - RowsReturnedRate: 58.441194M /sec

Profile 上面的 ExprComputeTime 是表示式執行的耗時,這個就表明了這個是 CASE WHEN 表示式執行的效率太低,可以排除SCANAGG的問題了

瓶頸出現在計算上那就好辦了,直接用perf看熱點程式碼在哪就行了

ps -ef|grep starrocks_be|grep stdpain|awk '{print $2}'
perf top -p $pid

很顯然,問題出在 VectorizedCaseExprColumnBuilder 上面。

向量化下Case When 執行原理

為了方便理解先簡單說一下CASE WHEN的處理邏輯,當然也可以看一下這個向量化傳送門

舉個例子:

CASE WHEN col1 + 10000 > 100000 then col2 + 200 else col2 - 200 END

首先需要把所有的分支都要執行一遍

  1. 執行表示式 col1 + 10000 > 100000 選擇列為 res1
  2. 執行表示式 col2 + 200 結果列為 res2
  3. 執行表示式 col2 - 200 結果列為 res3
  4. 通過選擇列 (res1) 來選擇結果列 (res2, res3) ,作為 res4 返回

這樣上面的每一個步驟都可以進行向量化計算

優化1 - 優化不必要的分支

ColumnBuilder是構建Column的一個幫助類,可以簡化很多邏輯,看一下ColumnBuilder的程式碼是這樣的

	void append(const DatumType& value) {
        _null_column->append(DATUM_NOT_NULL);
        _column->append(value);
    }

_null_column_column 這兩個成員可以認為是 std::vector<int8>

具體的呼叫是這樣的:

builder.reserve(size);
// 對於每一行來說
for (int row = 0; row < size; ++row) {
    // 先遍歷選擇列,來決定選的是哪一列
    int i = 0;
    while (i < view_size && !(when_viewers[i].value(row))) {
        i += 1;
    }
    // 插入資料
    if (!then_viewers[i].is_null(row)) {
        builder.append(then_viewers[i].value(row));
    } else {
        builder.append_null();
    }
}

這段程式碼問題很多

  1. 沒有必要的null值判斷,如果 then表示式不可能返回null,那也沒必要檢查null,另外即使then列真的可能返回null,那也不應該在迴圈中進行處理
  2. 沒有必要的迴圈套迴圈
  3. builder呼叫append雖然看上去是沒什麼問題,而且也事先分配了空間,但是vector在呼叫append的時候還是會檢查一下是否空間足夠這樣迴圈體裡面又多了一堆 if 分支

我們先特殊優化 只有一個when的情況來驗證我們的想法:

// 選擇向量
uint8_t select_vector[size];
// 先拿到選擇列
const auto& cond1_data = when_viewers[0].column() -> get_data();
// 構建選擇向量
for (int i = 0; i < size; i++) {
    select_vector[i] = cond1_data[i];
}
using ResCol = RunTimeColumnType<ResultType>;
auto res = ResCol::create();
// 先把常量展開成向量,後面再優化
auto then_0 = ColumnHelper::unpack_and_duplicate_const_column(size, then_columns[0]);
auto then_1 = ColumnHelper::unpack_and_duplicate_const_column(size, then_columns[1]);
auto& then0_data = ((ResCol*)then_0.get()) -> get_data();
auto& then1_data = ((ResCol*)then_1.get()) -> get_data();
auto& res_data = res -> get_data();
res_data.resize(size);
// 通過選擇向量來選擇
for(int i = 0;i < size; ++i) {
    res_data[i] = select_vector[i] ? then0_data[i]: then1_data[i];
}

跑一下看看

mysql> SELECT SUM(CASE WHEN LO_SUPPLYCOST + 10000 > 100000 then 1 else 0 END) FROM lineorder_flat;
+-------------------------------------------------------------------+
| sum(CASE WHEN `LO_SUPPLYCOST` + 10000 > 100000 THEN 1 ELSE 0 END) |
+-------------------------------------------------------------------+
|                                                         299718458 |
+-------------------------------------------------------------------+
1 row in set (4.26 sec)

果然,很有效果提升了一倍但是很多人會說 "我不滿意" (手工滑稽)

優化2 - SIMD

那就繼續看profile了

呃呃呃,上面顯示大頭還是VectorizedCaseExpr,看下具體熱點

我直接 ??? 這麼簡單的一個迴圈居然沒自動向量化?

for(int i = 0;i < size; ++i) {
    res_data[i] = select_vector[i] ? then0_data[i]: then1_data[i];
}

一頓操作之後(各種hint restrict)發現自動擋還是不行,所以還是手動擋吧

inline void avx2_select_if(uint8_t*& selector, char*& dst, const char*& a, const char*& b, int size) {
    const char* dst_end = dst + size;
    while (dst + 32 < dst_end) {
        __m256i loaded_mask = _mm256_loadu_si256(reinterpret_cast<__m256i*>(selector));
        loaded_mask = _mm256_cmpgt_epi8(loaded_mask,  _mm256_setzero_si256());
        __m256i loaded_a = _mm256_loadu_si256(reinterpret_cast<const __m256i*>(a));
        __m256i loaded_b = _mm256_loadu_si256(reinterpret_cast<const __m256i*>(b));
        __m256i res = _mm256_blendv_epi8(loaded_b, loaded_a, loaded_mask);
        _mm256_storeu_si256(reinterpret_cast<__m256i*>(dst), res);
        dst += 32;
        selector += 32;
        a += 32;
        b += 32;
    }
}

template <PrimitiveType TYPE, typename Container = typename RunTimeColumnType<TYPE>::Container>
void select_if(uint8_t* select_vector, Container& dst, const Container& a, const Container& b) {
    int size = dst.size();
    auto* start_dst = dst.data();
    auto* end_dst = dst.data() + size;

    auto* start_a = a.data();
    auto* start_b = b.data();

    if constexpr (std::is_same_v<RunTimeCppType<TYPE>, int8_t>) {
        avx2_select_if(select_vector, start_dst, start_a, start_b, size);
    }

    while (start_dst < end_dst) {
        *start_dst = *select_vector ? *start_a : *start_b;
        select_vector++;
        start_dst++;
        start_a++;
        start_b++;
    }
}

測試結果: 比較符合預期,證明思路沒問題

mysql> SELECT SUM(CASE WHEN LO_SUPPLYCOST + 10000 > 100000 then 1 else 0 END) FROM lineorder_flat;
+-------------------------------------------------------------------+
| sum(CASE WHEN `LO_SUPPLYCOST` + 10000 > 100000 THEN 1 ELSE 0 END) |
+-------------------------------------------------------------------+
|                                                         299718458 |
+-------------------------------------------------------------------+
1 row in set (1.69 sec)