跳到内容

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 加法

cpp
#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

可用多版本函数:

text
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

若编译器担心 outputleft/right overlap,就不能随意 reorder。C restrict 或 C++ compiler-specific assumptions 可帮助,但承诺错误会导致 undefined behavior。

更安全的是 API 明确不允许 overlap,并用 tests/sanitizers验证;不要仅为速度添加无法保证的 alias annotation。

比较、mask 与 selection

AVX2 比较 8 个 int32:

cpp
__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

text
true_mask = comparison_mask & validity_mask

但 compound SQL expressions 需要 (true_mask, null_mask) 或等价 representation。例如:

text
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:

text
(((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 骨架

cpp
#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 直觉:

text
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 形成可恢复状态变化。

Built with VitePress | Software Systems Atlas