向量化执行引擎:SIMD 如何将列式扫描吞吐提升 10 倍

发布时间:2026/6/26 2:18:23

向量化执行引擎:SIMD 如何将列式扫描吞吐提升 10 倍 向量化执行引擎SIMD 如何将列式扫描吞吐提升 10 倍一、一亿行过滤耗时 30 秒——标量执行的 CPU 浪费某分析型查询对 1 亿行订单表执行WHERE amount 1000 AND status PAIDClickHouse 耗时 0.8 秒而自研的标量执行引擎耗时 28 秒。Profile 显示CPU 70% 的时间消耗在分支预测失败和指令流水线停顿上。标量执行Volcano Model每次处理一行每行都需要虚函数调用、分支判断、数据逐字节搬运。现代 CPU 的 SIMD 寄存器AVX2 256bit / AVX-512 512bit一次可处理 8-16 个 32 位整数但标量执行完全浪费了这一能力。向量化执行引擎通过批量处理列数据将 CPU 利用率从 15% 提升到 80%。二、向量化执行的底层机制与 CPU 微架构2.1 标量 vs 向量化的本质区别标量执行Volcano Model每次next()返回一行向量化执行每次返回一批Batch/Chunk通常 1024-4096 行。核心差异不在批的大小而在向量化对同一列的连续数据执行同一操作使 CPU 可以用 SIMD 指令并行处理。flowchart LR subgraph 标量执行 A1[行1: amount500] -- B1{amount1000?} A2[行2: amount2000] -- B2{amount1000?} A3[行3: amount800] -- B3{amount1000?} A4[行4: amount3000] -- B4{amount1000?} end subgraph 向量化执行 C1[批量加载 8 个 amount 到 YMM 寄存器] -- D1[一条 vcmpps 指令同时比较 8 个值] D1 -- E1[位掩码过滤, 0 周期分支] end2.2 SIMD 指令集与数据对齐指令集寄存器宽度int32 并行数float64 并行数SSE4.2128bit42AVX2256bit84AVX-512512bit168数据对齐是 SIMD 性能的前提256bit 操作要求数据按 32 字节对齐未对齐访问触发跨缓存行惩罚约 3-5 倍延迟。列式存储天然满足对齐要求——同类型数据连续存储而行式存储需要额外的解包和重排。2.3 分支消除位掩码替代 if-else标量执行的if (amount 1000)在每行触发分支预测。向量化执行用比较指令生成位掩码用掩码压缩compress操作过滤数据完全消除分支// 标量: 分支预测 for (int i 0; i N; i) { if (amount[i] 1000) { // 分支, 预测失败代价 ~15 周期 output[count] amount[i]; } } // 向量化: 无分支 __m256i threshold _mm256_set1_epi32(1000); for (int i 0; i N; i 8) { __m256i vals _mm256_load_si256((__m256i*)(amount i)); __m256i cmp _mm256_cmpgt_epi32(vals, threshold); // 一条指令, 8 路并行 // 用掩码压缩, 无分支 uint32_t mask _mm256_movemask_ps(_mm256_castsi256_ps(cmp)); count _popcnt32(mask); }三、生产级向量化过滤与聚合实现3.1 C 向量化过滤引擎#include immintrin.h #include cstdint #include cstring #include memory #include vector #include algorithm #include stdexcept // 向量化列数据结构, 按 64 字节对齐分配 class AlignedColumn { public: explicit AlignedColumn(size_t capacity) : capacity_(capacity), size_(0) { // 64 字节对齐分配, 满足 AVX-512 要求 void* ptr nullptr; if (posix_memalign(ptr, 64, capacity * sizeof(int32_t)) ! 0) { throw std::runtime_error(内存分配失败); } data_ static_castint32_t*(ptr); } ~AlignedColumn() { free(data_); } // 禁止拷贝, 允许移动 AlignedColumn(const AlignedColumn) delete; AlignedColumn operator(const AlignedColumn) delete; AlignedColumn(AlignedColumn other) noexcept : data_(other.data_), capacity_(other.capacity_), size_(other.size_) { other.data_ nullptr; other.size_ 0; } int32_t* data() { return data_; } const int32_t* data() const { return data_; } size_t size() const { return size_; } void set_size(size_t s) { size_ s; } size_t capacity() const { return capacity_; } private: int32_t* data_; size_t capacity_; size_t size_; }; // 向量化过滤结果: 位掩码 选择向量 struct FilterResult { // 选择向量: 存储通过过滤的行号 std::vectoruint32_t selected; // 通过过滤的行数 uint32_t count 0; void add(uint32_t row_idx) { selected.push_back(row_idx); count; } }; // 向量化过滤引擎, 支持 AVX2 SSE4.2 降级 class VectorizedFilter { public: // 单列整数大于过滤 static FilterResult filter_gt_int32(const int32_t* data, size_t n, int32_t threshold) { FilterResult result; result.selected.reserve(n / 4); // 预估 25% 通过率 size_t i 0; #ifdef __AVX2__ // AVX2 路径: 每次 8 个 int32 const __m256i thresh_vec _mm256_set1_epi32(threshold); for (; i 8 n; i 8) { __m256i vals _mm256_load_si256(reinterpret_castconst __m256i*(data i)); __m256i cmp _mm256_cmpgt_epi32(vals, thresh_vec); // 提取位掩码, 每bit对应一个元素的比较结果 uint32_t mask _mm256_movemask_ps(_mm256_castsi256_ps(cmp)); // 根据掩码展开通过过滤的行号 while (mask ! 0) { uint32_t bit_pos __builtin_ctz(mask); // 最低位1的位置 result.add(static_castuint32_t(i bit_pos)); mask (mask - 1); // 清除最低位1 } } #endif // 标量尾处理: 处理剩余不足 8 个的元素 for (; i n; i) { if (data[i] threshold) { result.add(static_castuint32_t(i)); } } return result; } // 双列组合过滤: col1 threshold1 AND col2 value2 static FilterResult filter_and_int32( const int32_t* col1, const int32_t* col2, size_t n, int32_t threshold1, int32_t value2) { FilterResult result; result.selected.reserve(n / 8); size_t i 0; #ifdef __AVX2__ const __m256i thresh_vec _mm256_set1_epi32(threshold1); const __m256i value2_vec _mm256_set1_epi32(value2); for (; i 8 n; i 8) { __m256i v1 _mm256_load_si256(reinterpret_castconst __m256i*(col1 i)); __m256i v2 _mm256_load_si256(reinterpret_castconst __m256i*(col2 i)); // 两个条件分别计算, 再 AND 合并 __m256i cmp1 _mm256_cmpgt_epi32(v1, thresh_vec); __m256i cmp2 _mm256_cmpeq_epi32(v2, value2_vec); __m256i combined _mm256_and_si256(cmp1, cmp2); uint32_t mask _mm256_movemask_ps(_mm256_castsi256_ps(combined)); while (mask ! 0) { uint32_t bit_pos __builtin_ctz(mask); result.add(static_castuint32_t(i bit_pos)); mask (mask - 1); } } #endif for (; i n; i) { if (col1[i] threshold1 col2[i] value2) { result.add(static_castuint32_t(i)); } } return result; } }; // 向量化 SUM 聚合 class VectorizedAggregator { public: // 基于选择向量的向量化 SUM static int64_t sum_selected(const int32_t* data, const FilterResult filter) { int64_t total 0; size_t i 0; const uint32_t* sel filter.selected.data(); size_t count filter.count; #ifdef __AVX2__ // 累加器用 4 路 int64, 避免溢出 __m256i acc _mm256_setzero_si256(); for (; i 4 count; i 4) { // 收集 4 个选中位置的值 int32_t vals[4] { data[sel[i]], data[sel[i1]], data[sel[i2]], data[sel[i3]] }; // 符号扩展到 int64 并累加 __m128i v32 _mm_loadu_si32(vals); __m256i v64 _mm256_cvtepi32_epi64(v32); acc _mm256_add_epi64(acc, v64); } // 水平求和 alignas(32) int64_t tmp[4]; _mm256_store_si256(reinterpret_cast__m256i*(tmp), acc); total tmp[0] tmp[1] tmp[2] tmp[3]; #endif // 标量尾处理 for (; i count; i) { total data[sel[i]]; } return total; } };3.2 性能对比 Benchmark在 1 亿行 int32 列上执行WHERE amount 1000实现方式耗时CPU 利用率分支预测失败率标量 Volcano28.3s15%12.7%SSE4.2 向量化4.1s62%0.3%AVX2 向量化2.8s78%0.1%AVX-512 向量化1.9s85%0.1%四、向量化执行的边界与架构妥协4.1 选择率对性能的影响向量化过滤的性能收益与选择率强相关。当选择率低于 5% 时压缩操作compress的写回开销极低收益最大。当选择率高于 80% 时压缩操作几乎复制全部数据收益递减。极端场景选择率 100%全通过向量化反而比标量多一次无意义的掩码计算。4.2 数据类型限制SIMD 对定长数值类型int32/float64效果显著但对变长类型VARCHAR、JSON几乎无能为力。字符串比较仍需逐字节处理无法向量化。ClickHouse 的做法是将低基数字符串编码为字典 ID对 ID 做向量化过滤。4.3 编译与部署复杂度AVX-512 仅在较新 CPUSkylake-X 及以后上可用。运行时需要 CPUID 检测并动态分发到对应代码路径。编译时需开启-mavx2/-mavx512f但生成的二进制无法在旧 CPU 上运行。生产方案编译多个版本运行时动态加载或使用ifunc机制做函数级分发。4.4 禁用场景行式存储引擎数据非连续存储向量化加载效率极低高选择性点查命中索引返回 100 行向量化批处理的开销大于收益复杂嵌套 UDFUDF 内部逻辑无法向量化成为瓶颈五、总结向量化执行引擎的核心收益来自三个维度SIMD 指令并行处理、分支消除位掩码替代 if-else、数据局部性列式连续存储。生产级实现需要处理数据对齐、SIMD 指令集降级、选择向量管理等工程细节。向量化并非银弹其收益受选择率、数据类型、存储格式约束。务实的路径是在列式存储 分析型查询场景下优先向量化在行式存储 点查场景下保持标量执行用 Benchmark 数据而非直觉决定优化方向。

相关新闻