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 采用了两阶段策略:

  1. Partial Aggregation:每个 Pipeline 内部先做局部聚合,减少数据量
  2. 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 的整体设计,可以总结出几个核心的工程设计原则:

  1. "库而非框架":Velox 定位为可嵌入的库,不强制用户使用特定的调度或 SQL 解析层
  2. "与 Arrow 共生":基于 Apache Arrow 格式实现零拷贝互操作,降低集成成本
  3. "编译期准备 + 运行时特化":避免 JIT 的延迟,同时保持足够的灵活性
  4. "硬件中立":统一的 HAL 保证了代码的长期演进能力
  5. "测试驱动":Velox 的测试覆盖率极高,支持 Fuzzing、Property-based Testing

Velox 体现了"系统软件的软件工程化"趋势——通过精细的抽象层次划分、严格的接口定义和充分的硬件抽象,将原本耦合在特定引擎中的执行逻辑抽取出来,成为所有数据处理引擎共享的基础设施。

对于系统编程的从业者来说,Velox 是一个优秀的学习案例——它展示了如何用现代 C++ 实现高性能、可扩展、跨硬件的执行引擎,也预示着数据处理领域从"引擎间重复造轮子"走向"共享向量化执行层"的技术演进方向。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部