10.2 SIMD Kernel、运行时分派与数值语义
向量化执行器已经成批处理数据,性能工程师还想让一条 CPU 指令同时完成多个元素的计算。
SIMD 让一条 instruction 操作多个 lanes。真正可用的数据库 kernel 还必须处理 alignment、tail、NULL mask、selection、overflow、floating reduction order 和不同 CPU features。写出 _mm256_add_* 只是开始。
ISA 与 lanes
- x86 SSE2:128-bit integer/floating vectors;
- AVX:256-bit floating operations;
- AVX2:扩展 256-bit integer 与 gather 等;
- AVX-512:512-bit vector + richer mask/compress subsets;
- ARM NEON:常见 128-bit SIMD;
- ARM SVE/SVE2:vector-length-agnostic programming model。
CPU 宣称 AVX-512 不代表所有 AVX-512 subsets 都有,也不保证比 AVX2 快;frequency、ports、memory bandwidth、downclock 与 kernel mix 都影响结果。
binary 若无 runtime dispatch就在不支持 ISA 的 CPU 执行,会 illegal instruction。容器/VM migration 与 heterogeneous cluster 更要谨慎。
安全的 AVX2 加法
#include <cstddef>
#include <immintrin.h>
void add_f32_avx2(
const float* left,
const float* right,
float* output,
std::size_t count) {
std::size_t i = 0;
const std::size_t vector_end = count - (count % 8);
for (; i < vector_end; i += 8) {
const __m256 a = _mm256_loadu_ps(left + i);
const __m256 b = _mm256_loadu_ps(right + i);
_mm256_storeu_ps(output + i, _mm256_add_ps(a, b));
}
for (; i < count; ++i) {
output[i] = left[i] + right[i];
}
}原稿中的 for (i=0; i<n; i+=8) 会在 n 不是 8 的倍数时越界读取,然后尾循环也无法修复。main loop 必须只处理完整 vectors。
loadu 支持未对齐地址。aligned load intrinsic 要求相应 alignment,违反是 undefined behavior/可能 fault。现代 CPU 上 unaligned load 是否更慢取决于是否跨 cache line/page等,不能一概说“带 u 必然慢”。
Runtime dispatch
可用多版本函数:
scalar baseline
SSE2 version
AVX2 version
AVX-512 version进程启动或首次调用时检测 OS+CPU support,选择 function pointer。x86 除 CPUID feature bits 外,AVX 还需 OS 保存 extended state(XGETBV/编译器 builtin通常帮助处理)。不要自己只查一个 CPUID bit。
GCC/Clang 的 function multiversioning、target attributes 或平台 dispatch library 可减少手写错误;build flags 与最低 CPU baseline 要记录。
Auto-vectorization
compiler 更容易 vectorize:
- simple countable loop;
- contiguous non-aliasing pointers;
- known alignment/stride;
- no loop-carried dependency;
- operation semantics允许重排;
- branch 可转 mask/select。
阻碍:
- possible pointer alias;
- function calls/virtual dispatch;
- variable-length strings;
- unpredictable gather/scatter;
- exact floating order;
- overflow/trap semantics;
- early exits。
使用 vectorization report 和 disassembly 证明,不要看到 -O3 就宣称已生成 SIMD。
Alias 与 restrict
若编译器担心 output 与 left/right overlap,就不能随意 reorder。C restrict 或 C++ compiler-specific assumptions 可帮助,但承诺错误会导致 undefined behavior。
更安全的是 API 明确不允许 overlap,并用 tests/sanitizers验证;不要仅为速度添加无法保证的 alias annotation。
比较、mask 与 selection
AVX2 比较 8 个 int32:
__m256i values = _mm256_loadu_si256(
reinterpret_cast<const __m256i*>(input + i));
__m256i threshold = _mm256_set1_epi32(50000);
__m256i lanes = _mm256_cmpgt_epi32(values, threshold);
int bitmask = _mm256_movemask_ps(_mm256_castsi256_ps(lanes));bitmask 每 bit 对应一个 lane。接下来可:
- bit scan 生成 selected indices;
- 保留 bitmask 给下游;
- 用 table/permute compact;
- 高 selectivity 时走 dense path。
把 32-bit comparison lanes 直接 store 到 uint8_t mask[8] 会写越界或得到错误布局。
NULL mask
若 validity 是 bit-packed,SIMD predicate mask 必须与 valid bits结合。对于 column > constant:
true_mask = comparison_mask & validity_mask但 compound SQL expressions 需要 (true_mask, null_mask) 或等价 representation。例如:
FALSE AND NULL = FALSE
TRUE AND NULL = NULL
TRUE OR NULL = TRUE
FALSE OR NULL = NULL不能只一路 AND validity 处理所有逻辑 operators。
Integer overflow
C/C++ signed overflow 是 undefined behavior,数据库 integer arithmetic 通常要求检测并报错,或为 aggregate提升 accumulator type。compiler 不能在 -fstrict-overflow assumptions 下把 SQL overflow checks优化掉。
vector add 可通过 widened lanes、compare/sign logic 或 ISA overflow detection pattern实现;结果必须与 scalar database semantics 一致。
Floating aggregation
scalar left fold:
(((a0 + a1) + a2) + a3) ...SIMD 用多个 partial accumulators,改变 addition order。floating addition 非 associative,因此 low bits/rounding 可不同;NaN、signed zero 与 overflow 也需定义。
数据库可允许非确定的 parallel floating aggregate,或采用 pairwise/Kahan/decimal等更稳定路径。无论哪种都应写进契约和 tests。-ffast-math 会放宽 IEEE assumptions,不应未经审查用于 SQL kernel。
SIMD sum 骨架
#include <cstddef>
#include <immintrin.h>
float sum_f32_avx2(const float* values, std::size_t count) {
__m256 vector_sum = _mm256_setzero_ps();
std::size_t i = 0;
const std::size_t vector_end = count - (count % 8);
for (; i < vector_end; i += 8) {
vector_sum = _mm256_add_ps(
vector_sum,
_mm256_loadu_ps(values + i));
}
alignas(32) float lanes[8];
_mm256_store_ps(lanes, vector_sum);
float total = 0.0F;
for (float lane : lanes) {
total += lane;
}
for (; i < count; ++i) {
total += values[i];
}
return total;
}这个示例教学 tail/alignment,不处理 NULL,也不保证与 scalar left fold bit-identical。production kernel 应有 ISA dispatch、tests、NaN policy 与更好的 horizontal reduction。
Memory-bound 与 compute-bound
若 scan 只做一条 add,memory bandwidth 已饱和,理论 8 lanes 不会带来 8× speedup。可用 roofline 直觉:
attainable performance <= min(compute peak,
memory bandwidth × arithmetic intensity)compression 会减少 bytes,却增加 decode compute;有时 SIMD decode 同时改善两者。hash probe/random gather 常受 cache-miss latency限制。
AVX/SSE transition 与 frequency
旧 x86 微架构混用 legacy SSE 与 AVX register state 可能有 transition penalty,编译器常插 vzeroupper。wide-vector frequency behavior 也因微架构和 workload 不同。
不要在教材写固定惩罚周期或声称 AVX-512 必然降频。用目标 CPU hardware counters、frequency与 end-to-end workload测量。
Benchmark
至少包含:
- aligned/unaligned、跨 cache line/page;
- size 从 L1-resident 到 memory-bandwidth bound;
- selectivity 0%、1%、50%、99%、100%;
- NULL density/pattern;
- tails 0–vector_width-1;
- skew/gather/string;
- scalar baseline、auto-vectorized、intrinsics;
- result bit correctness、overflow/NaN;
- runtime dispatch cold/warm;
- end-to-end query,而非只报 microkernel。
验收清单
- [ ] SIMD loop 不越界并处理 tail;
- [ ] unsupported CPU 有 scalar/baseline path;
- [ ] mask layout 与 destination type 一致;
- [ ] NULL 保留三值逻辑;
- [ ] integer overflow 与 floating order 有契约;
- [ ] 用 compiler report/disassembly 证明 vectorization;
- [ ] 同时测 cache-resident 和 memory-bound;
- [ ] microkernel 收益在完整 query 中复核。
本章小结
向量化是执行架构,SIMD 是其中可用的硬件能力。真正可靠的加速来自合适数据表示、批处理、mask/selection、runtime dispatch 与严格数值语义,而不是把标量循环机械换成 intrinsic。下一章将从 CPU 执行回到 transaction:多个操作如何以 atomicity、consistency、isolation 与 durability 形成可恢复状态变化。