Velox 向量化执行引擎架构深度拆解:Meta 的异构计算数据处理基础设施
Velox 是 Meta 开源的 C++ 向量化执行引擎,它为 Presto、Spark、PyArrow 等数据处理系统提供高性能的本地执行能力。本文深入剖析 Velox 的核心架构设计、表达式求值机制、运算符模型以及与异构计算硬件的集成策略,带你理解现代数据处理引擎的底层实现原理。
1. 为什么需要 Velox?
现代数据处理引擎普遍面临一个核心矛盾:SQL 引擎需要处理 TB 级甚至 PB 级的数据,但传统的行式处理和逐行迭代方式存在严重的 CPU 缓存未命中和分支预测失败问题。Velox 正是为了解决这一问题而诞生。
在 Velox 出现之前,Meta 内部的数据处理面临以下挑战:
- JVM 引擎(如 Presto JVM)因 GC 暂停和对象头开销导致内存效率低下
- 不同引擎(Presto、Spark、PyTorch Data)各自实现执行逻辑,重复造轮子
- 向量化和代码生成难以统一抽象
- 异构硬件(GPU、FPGA、AI 加速器)无法复用同一套执行层
Velox 的定位是"可重用的向量化执行引擎库",而非完整的 SQL 引擎。它提供了一套纯 C++ 实现的执行层,任何上层引擎都可以通过嵌入 Velox 来获得高性能的向量化执行能力。
2. 核心架构概览
Velox 的架构自顶向下可以分为以下几个层次:
┌─────────────────────────────────────────────────┐
│ 上层引擎 (Presto/Spark/PyArrow) │
├─────────────────────────────────────────────────┤
│ Plan 转换层 (PlanTranslator) │ SQL Plan → Velox Plan
├─────────────────────────────────────────────────┤
│ 核心执行层 (Core Execution) │
│ ┌─────────┐ ┌──────────┐ ┌────────────────┐ │
│ │DuckDB/ │ │Vector │ │Pipeline Driver │ │
│ │Arrow │ │Expr Eval │ │(Task/Operator) │ │
│ └─────────┘ └──────────┘ └────────────────┘ │
├─────────────────────────────────────────────────┤
│ 内存管理层 (Memory Management) │
│ ┌─────────┐ ┌──────────┐ ┌────────────────┐ │
│ │Memory │ │Arrow │ │String/Buffer │ │
│ │Pool │ │Columnar │ │Buffer Manager │ │
│ └─────────┘ └──────────┘ └────────────────┘ │
├─────────────────────────────────────────────────┤
│ 硬件抽象层 (Hardware Abstraction) │
│ ┌─────────┐ ┌──────────┐ ┌────────────────┐ │
│ │CPU SIMD │ │CUDA/GPU │ │Bitonic Sort/ │ │
│ │Kernel │ │Kernel │ │PrefixSum │ │
│ └─────────┘ └──────────┘ └────────────────┘ │
└─────────────────────────────────────────────────┘
与传统的火山模型(Volcano Model)不同,Velox 采用向量化批处理模式,每次处理一批数据(通常 1024 或更多行),配合列式内存布局和现代 CPU 的 SIMD 指令,实现了极高的吞吐量。
3. 列式内存模型与 Vector 抽象
Velox 的核心数据结构是 Vector,它对应一列数据的批量存储。Velox 基于 Apache Arrow 格式,但做了许多扩展以支持执行期的特殊需求。
// Velox Vector 类型层次
class Vector {
TypePtr type_; // 列的数据类型
BufferPtr nulls_; // 可选的 null bitmap
BufferPtr values_; // 实际数据缓冲区
vector<BufferPtr> strings_; // 变长字符串的额外缓冲区
size_t length_; // 当前批处理的行数
VectorEncoding::Simple encoding_; // 编码方式
};
Velox 支持丰富的编码方式:
- FLAT:扁平编码,最常见的列式布局
- DICTIONARY:字典编码,适合低基数列
- CONSTANT:常量编码,整列只有一个值
- SEQUENCE:序列编码,用于递增数列
这种编码多样性使得 Velox 可以在不同数据分布下选择最优的执行策略。例如,对于一个低基数的 GROUP BY 列,Velox 自动维持字典编码,避免了重复值的多余计算。
4. 表达式求值:零开销的 Codegen 策略
Velox 的表达式求值引擎是其性能的核心。它采用了一种运行时特化 + SIMD 向量化的混合策略:
// Velox 表达式求值的执行流程
// 1. 表达式树构建 (ExprSet)
auto expr = makeFlatExpr("price * quantity * (1 - discount)", pool);
// 2. 编译期特殊化 (Specialized Codegen)
// - 自动消除 null 分支
// - 识别常量折叠
// - 向量化循环(每次处理 8/16 行)
// 3. 执行时自适应 (Adaptive Execution)
// - 根据编码选择最优内核
// - 字典传播 (Dictionary Propagation)
// - 常量跳过 (Constant Skip)
与完全的 JIT 编译(如 LLVM-based Codegen)不同,Velox 采用"预编译内核 + 运行时选择"的策略。对于每种表达式模式,Velox 在初始化阶段就预编译好多个特化版本(无 null、全 null、混合 null 等),运行时根据实际数据的 null 分布选择对应版本。
这种策略的优势在于:
- 避免了 JIT 编译的延迟
- 内核经过充分优化,可能包含 SIMD 指令
- 对于热点表达式路径,可以获得接近手写汇编的性能
5. 运算符模型与 Pipeline 执行
Velox 的执行采用Pipeline + Driver模型:
// Velox Task 的执行模型
class Task {
vector<unique_ptr<Pipeline>> pipelines_;
};
class Pipeline {
vector<unique_ptr<Operator>> operators_;
Driver* driver_; // 执行上下文
};
// Operator 类型示例
class FilterProject : public Operator { ... };
class HashAggregation : public Operator { ... };
class HashJoin : public Operator { ... };
class OrderBy : public Operator { ... };
class PartitionedOutput : public Operator { ... };
每个 Pipeline 包含一系列 Operator,由 Driver 顺序执行。Pipeline 之间通过 Exchange(Shuffle/Data Broadcast)连接,实现了并行处理。
值得注意的是 Velox 的同步执行模型——它不使用异步回调或协程。这一设计选择使得代码更简单、更可预测,也更容易与上层引擎的调度模型对齐。
6. 聚合与 Join 的向量化实现
6.1 Hash Aggregation
Velox 的 Hash Aggregation 采用了两阶段策略:
- Partial Aggregation:每个 Pipeline 内部先做局部聚合,减少数据量
- Final Aggregation:按 GROUP BY key 重新分区后,合并局部结果
对于 Memory 受限场景,Velox 支持Spill to Disk——当哈希表超过内存配额时,自动将中间结果溢出到磁盘,保证了生产级稳定性。
6.2 Hash Join
Velox 的 Hash Join 实现了经典的 Build-Probe 模型,并针对向量化场景做了大量优化:p>
- Fast Path:对于整数 Join Key,直接使用 Flat Hash Map,避免字符串哈希开销
- Runtime Filter:在 Build 阶段动态生成 Bloom Filter,在 Probe 阶段过滤掉不匹配的 Build 侧数据
- Multimap Join:处理一对多 Join 时的批量展开优化
// Velox Hash Join 伪代码
hashJoin(probeVectors, buildTable):
// 1. 构建 Runtime Filter
if (buildTable.rowCount() <= runtimeFilterThreshold):
bloomFilter = buildBloomFilter(buildTable)
applyRuntimeFilter(probeVectors, bloomFilter)
// 2. 向量化的 Probe 操作
for each batch in probeVectors:
// 向量化 hash 计算
hashes = computeHashes(batch[joinKey], batch.size())
// 向量化探测
matches = probeHashTable(batch, hashes)
// 展开匹配结果 (行复制)
output = flattenMatches(batch, matches)
7. 内存管理:避免 GC 的 C++ 策略
Velox 采用了一套精细的内存管理系统。核心组件包括:
- Memory Pool:层次化的内存池,支持配额和统计
- Reference Counting:使用引用计数管理 Buffer 生命周期
- Buffer Manager:追踪所有大内存分配,提供全局内存视图
// Velox 内存管理示例
class MemoryPool {
MemoryPool* parent_; // 父池,形成树状结构
uint64_t capacity_; // 容量上限
uint64_t usedBytes_; // 当前使用量
void* allocate(size_t size);
void free(void* p, size_t size);
// 支持回调,当配额使用率超过阈值时触发
std::function<void(double)> usageCallback_;
};
Velox 还支持String Buffer的特殊管理——对于变长字符串,Velox 使用了一个专门的 String Buffer Allocator,支持短字符串内联(SSO)和长字符串堆分配,大幅减少了字符串操作的开销。
8. 异构计算:GPU/FPGA 的统一执行
Velox 最具前瞻性的设计之一是其硬件抽象层(HAL)。它允许 Operator 的实现在 CPU、GPU、FPGA 之间无缝切换:
// Velox 硬件抽象层接口
class ExecDevice {
virtual VectorPtr execute(ExprSet& expr, const std::vector<VectorPtr>> inputs) = 0;
virtual bool supports(const TypeFamily& type) = 0;
};
class CPUDevice : public ExecDevice {
// 使用 SIMD 指令 (SSE/AVX2/AVX-512) 或预编译内核
};
class CUDADevice : public ExecDevice {
// 使用 CUDA Kernel (cuDF、cuBLAS 等)
};
Velox 在与 GPU 集成方面的主要策略是:
- Zero-Copy 交换:CPU 和 GPU 之间通过统一内存(Unmanaged Memory)共享 Arrow Buffer
- Operator 粒度调度:不同 Operator 可以运行在不同设备上——例如扫描和过滤在 GPU 上做,而 Hash Join 在 CPU 上做
- 自适应切换:根据数据量大小和网络传输成本,自动决定是否将计算发送到 GPU
9. 上层引擎集成
Velox 已经集成到多个重要系统中:
| 系统 | 集成方式 | 性能提升 |
|---|---|---|
| Presto (Velox Native) | 替换 JVM 实现 | 3-10x 吞吐量提升 |
| Spark (Velox Backend) | 替换 Tungsten 执行 | 2-5x 算子级加速 |
| PyArrow | 作为计算后端 | 可以直接使用 Arrow 格式 |
| PyTorch DataLoader | 加速数据预处理 | 降低训练数据加载瓶颈 |
以 Presto Velox Native 为例,它在相同硬件上将大多数 SQL 查询的 CPU 效率提升了 3-10 倍,同时内存使用减少了 50%-80%——这对于大规模数据仓库的运营成本是巨大的优化。
10. 实战:编写一个简单的 Velox 程序
下面是一个使用 Velox C++ API 进行简单计算的示例:
#include "velox/parse/Expressions.h"
#include "velox/exec/tests/utils/PlanBuilder.h"
#include "velox/vector/FlatVector.h"
#include "velox/common/memory/Memory.h"
using namespace facebook::velox;
int main() {
// 1. 初始化内存系统
memory::MemoryManager<memory::MemoryAllocator>::init({});
auto pool = memory::memoryManager()->.addLeafPool("demo");
// 2. 创建输入数据 (列式)
auto makeRow = [](vector<int64_t> ids, vector<double> values) {
auto vecIds = std::make_shared<FlatVector<int64_t>>(
pool.get(), BIGINT(), nullptr, ids.size(),
Buffer::create<int64_t*>(ids.data(), ids.size(), ...));
auto vecVals = std::make_shared<FlatVector<double>>(
pool.get(), DOUBLE(), nullptr, values.size(),
Buffer::create<double*>(values.data(), values.size(), ...));
auto rowVec = std::make_shared<RowVector>(
pool.get(),
ROW({BIGINT(), DOUBLE()}),
nullptr,
ids.size(),
vector<VectorPtr>{vecIds, vecVals});
return std::make_shared<exec::test::Cursor>(rowVec);
};
// 3. 构建执行计划: SELECT id, value * 1.1 AS adjusted FROM data WHERE id > 10
auto cursor = makeRow({1, 15, 20, 5, 100}, {10.0, 20.0, 30.0, 40.0, 50.0});
auto plan = exec::test::PlanBuilder()
.tableScan({"id", "value"})
.filter("id > 10")
.project({"id", "value * 1.1 AS adjusted"})
.planFragment();
// 4. 执行并收集结果
auto result = exec::test::TaskCursor::create(plan);
while (auto batch = result->next()) {
auto ids = batch->childAt(0)->asFlatVector<int64_t>();
auto vals = batch->childAt(1)->asFlatVector<double>();
for (auto i = 0; i < batch->size(); ++i) {
std::cout << ids->valueAt(i) << ": " << vals->valueAt(i) << "\n";
}
}
// 输出:
// 15: 22
// 20: 33
// 100: 55
}
11. Velox 的设计哲学与启示
回顾 Velox 的整体设计,可以总结出几个核心的工程设计原则:
- "库而非框架":Velox 定位为可嵌入的库,不强制用户使用特定的调度或 SQL 解析层
- "与 Arrow 共生":基于 Apache Arrow 格式实现零拷贝互操作,降低集成成本
- "编译期准备 + 运行时特化":避免 JIT 的延迟,同时保持足够的灵活性
- "硬件中立":统一的 HAL 保证了代码的长期演进能力
- "测试驱动":Velox 的测试覆盖率极高,支持 Fuzzing、Property-based Testing
Velox 体现了"系统软件的软件工程化"趋势——通过精细的抽象层次划分、严格的接口定义和充分的硬件抽象,将原本耦合在特定引擎中的执行逻辑抽取出来,成为所有数据处理引擎共享的基础设施。
对于系统编程的从业者来说,Velox 是一个优秀的学习案例——它展示了如何用现代 C++ 实现高性能、可扩展、跨硬件的执行引擎,也预示着数据处理领域从"引擎间重复造轮子"走向"共享向量化执行层"的技术演进方向。

发表评论 取消回复