Branch 向量化
阿新 • • 發佈:2021-10-24
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
表示式執行的效率太低,可以排除SCAN
和AGG
的問題了
瓶頸出現在計算上那就好辦了,直接用perf看熱點程式碼在哪就行了
ps -ef|grep starrocks_be|grep stdpain|awk '{print $2}'
perf top -p $pid
很顯然,問題出在 VectorizedCaseExpr
和ColumnBuilder
上面。
向量化下Case When 執行原理
為了方便理解先簡單說一下CASE WHEN
的處理邏輯,當然也可以看一下這個向量化傳送門
舉個例子:
CASE WHEN col1 + 10000 > 100000 then col2 + 200 else col2 - 200 END
首先需要把所有的分支都要執行一遍
- 執行表示式
col1 + 10000 > 100000
選擇列為 res1 - 執行表示式
col2 + 200
結果列為 res2 - 執行表示式
col2 - 200
結果列為 res3 - 通過選擇列 (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();
}
}
這段程式碼問題很多
- 沒有必要的null值判斷,如果 then表示式不可能返回null,那也沒必要檢查null,另外即使then列真的可能返回null,那也不應該在迴圈中進行處理
- 沒有必要的迴圈套迴圈
- 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)