列式存储需内存对齐(如AVX2的32字节)、预分配大内存池、按列切分偏移,数值列块对齐排布,字符串用offset数组,布尔列bit-packed,空值用独立bitset;向量化表达式求值应采用模板元编程避免虚函数开销;Filter+Project流水线须逐块流式处理并用SIMD掩码传递状态;冷数据扫描宜用mmap+page fault机制配合预读优化。

列式存储的核心结构怎么设计才不拖慢向量化执行
列式存储不是简单把每列存成一个 std::vector 就完事。关键在于内存布局必须对齐 SIMD 指令宽度(如 AVX2 的 32 字节),且避免指针跳转和 cache line 分裂。实际中,std::vector 默认分配的内存虽连续,但不同列之间无对齐保证,push_back 还会引发重分配——这会让向量化循环在运行时频繁触发 page fault 或 cache miss。
推荐做法是预分配一块大内存池(std::aligned_alloc(32, total_size)),再按列切分偏移;每列数据用 std::span<const t></const> 或自定义 ColumnView 封装,只存起始地址 + 长度 + 对齐信息。字符串等变长类型需额外维护 offset 数组(类似 Apache Arrow 的 StringViewArray),避免指针数组间接访问。
- 数值列:直接按
T[1024]块对齐排布,长度非 32 字节倍数时补零(不影响计算,SIMD mask 可处理) - 布尔列:用 bit-packed 存储(1 byte = 8 个值),向量化时用
_mm256_movemask_epi8快速转成掩码整数 - 空值(null):单独用 bitset 列(
std::vector<uint8_t></uint8_t>),与主数据严格同长,避免混合存储引入分支
向量化表达式求值怎么绕过虚函数调用开销
C++ 模板元编程是目前最实用的方案。用 std::visit 或虚函数做表达式树遍历,每次运算都要查虚表或 std::any 解包,实测在 1M 行上比模板展开慢 3–5 倍。正确路径是把算子类型(AddOp, FilterEq)、数据类型(int32_t, float64_t)全作为模板参数推导,生成特化函数。
例如写一个向量化比较函数:template<typename t typename op> void eval_batch(const T* lhs, const T* rhs, bool* out, size_t len)</typename>,其中 Op 是 std::equal_to<t></t> 这类可内联的函数对象。编译器能完全展开循环、自动向量化(GCC/Clang 加 -O3 -mavx2 即可),无需手写 intrinsics。
立即学习“C++免费学习笔记(深入)”;
- 避免
std::function或std::any在 hot path 上出现 - 表达式树构建阶段就完成类型绑定(e.g.
BinaryExpr<int32_t std::plus>></int32_t>),运行时零反射开销 - 支持运行时决定 batch size(如 1024 或 4096),但内部循环必须是编译期常量,否则 auto-vectorization 失效
如何让 Filter + Project 流水线真正避免中间结果物化
典型错误是先执行 filter() 得到一个 std::vector<uint32_t></uint32_t> 行号列表,再用它去索引所有列——这既破坏 cache 局部性,又让后续计算无法向量化。真正的列式流水线必须保持“逐块流式处理”:一次加载一批(如 256 行)的各列数据,用 SIMD 掩码实时过滤,被筛掉的元素不参与后续计算,但内存位置仍连续。
核心技巧是使用 mask 传递状态。比如 FilterEqKernel<int32_t>::apply()</int32_t> 返回一个 __m256i 掩码,下游 ProjectKernel 直接用该掩码控制 store 操作(_mm256_maskstore_ps)。整个过程不生成新行号数组,也不改变原始列内存布局。
- 所有 kernel 必须接受输入
const T*、输出T*和__m256i mask三参数,mask 为 0 表示跳过该 slot - 当 filter 选择率极低(_mm256_i32gather_epi32)反而更慢,应退化为标量 loop + branch prediction
- 多个 filter 组合不能嵌套调用,必须融合成单个 kernel(e.g.
a > 10 && b == "foo"→ 一个融合的 AVX2 函数)
为什么 mmap + page fault 机制比 fread 更适合冷数据扫描
面对 GB 级列存文件(如 Parquet 分块),用 fread 同步读取会阻塞整个查询线程,且难以控制预读粒度。Linux 的 mmap(MAP_PRIVATE | MAP_POPULATE) 让内核按需加载 page,配合 madvise(MADV_WILLNEED) 提前触发预读,实测在 SSD 上吞吐提升 2–3 倍。
但要注意:mmap 地址空间碎片、大页(Huge Page)未启用、或 NUMA 节点跨域访问都会导致性能抖动。生产环境必须检查 /proc/sys/vm/transparent_hugepage 是否为 always,并用 numactl --membind=0 绑定内存节点。
- 列数据文件必须按列对齐存储(e.g. int32 列起始地址 % 32 == 0),否则 mmap 后的地址无法直接用于 AVX 指令
- 不要对 mmap 区域调用
memset或写入未mprotect的只读段,会触发 SIGBUS - 小文件(readahead() +
posix_fadvise(POSIX_FADV_DONTNEED),避免 mmap 的 TLB 开销
真正难的不是实现单个向量化算子,而是让 filter、project、join、aggregation 各环节的内存视图、生命周期、null 处理逻辑全部对齐——稍有不一致,就会在 10 亿行数据上放大成秒级延迟。别迷信 benchmark 数字,先拿 perf record 看清 cache-misses 和 branch-misses 占比。



















