Macduan Notes

Velox: Meta’s Unified Execution Engine

Velox 把查询执行中可复用的部分做成 C++ 库:类型与向量、表达式与函数、执行算子、connector、内存管理和 spill。宿主系统把自己的计划与运行环境接进来,共享这些执行能力。

执行流程与相关实现核对于 2026-09-29,Velox 源码版本为 48883e8521b2。下文源码节选、历史资料和宿主集成引用各自注明版本;教学输入用于解释状态变化,未作为性能基准运行。

1. 统一执行库的能力与宿主职责

Velox 是可嵌入宿主系统的向量化执行库。它为查询片段提供类型、向量、表达式、函数、算子、数据访问接口和资源管理能力。SQL 解析、全局优化、分布式任务放置等工作仍需要宿主及其集成层负责。所谓统一执行,是让多个系统复用执行层的实现与优化,而不是让它们具有完全相同的前端与调度策略。

层面 Velox 提供的能力 集成时仍需确定的内容
数据表示 Type 与多种 Vector 编码 宿主类型、文件 schema 与逻辑语义的映射
表达式与函数 批量求值、函数执行接口及相关优化 方言、函数注册、转换和错误语义
算子执行 计划片段的本地执行与 Driver 调度 宿主计划转换、任务并行度和跨节点连接
数据访问 Connector、Reader/Writer 与 exchange 扩展点 存储系统、元数据、协议和访问策略
资源管理 内存记账、仲裁、回收与 spill 机制 查询资源策略、配置及运行环境

数据在这套系统中同时具有逻辑含义和具体表示。Type 描述 schema 与执行所需的类型信息,Vector 组织一批值及其编码,表达式与算子按选中行处理这些值,Task/Driver 协调运行与等待。后面的组件分析都可以放回这条执行链中理解。

本文同时保留论文的原始动机、实验与历史背景,以及固定源码快照的实现补充。论文展示的是特定系统和负载下的结果;当前接口和工程细节以相应源码补充为准,二者不能合并成一项对所有宿主的性能承诺。

图 1:Velox 的复用边界;宿主控制计划和生命周期,执行层通过接口访问存储与交换。
图 1:Velox 的复用边界;宿主控制计划和生命周期,执行层通过接口访问存储与交换。

这篇文章最初是 2022 年 Velox 论文的阅读笔记。2026-09-19 重构后,正文按 Velox 1d1b76567870 解释当前实现;论文的动机和实验单独保留为历史背景。

代码片段分别标明源码节选或流程示意;流程示意省略统计、异常包装与无关分支,不是可独立编译的程序。历史资料与宿主集成保留各自版本,不能据此推断它们组成了经过构建验证的发行版本。

本文保留早期论文与设计介绍中的全部背景、例子和实验讨论。历史能力、实验性 Codegen 与性能数字以当时论文为语境;各章的源码核对说明 2026-09-20 固定提交中的对应实现,不能把历史实验当成本机复测。

stack-transform:根据原图完整重绘,正文说明当前语义

2. Velox 的定位与基本能力

Velox是Meta开发的扩展性强,高性能的C++执行引擎加速库,提供可重用、可扩展、高性能且与方言无关的数据处理组件,用于构建执行引擎和增强数据管理系统。该库在很大程度上依赖向量化和适应性,并从根本上设计为支持对复杂数据类型的高效计算,因为这些类型在现代工作负载中非常普遍。Velox被用于Meta内部的批处理,流处理,交互式查询,和机器学习组件(Presto, Spark, PyTorch, XStream, F3, FBETL等)。Velox不提供语言前端,比如SQL,Dataframe等,它使用一个充分优化后的查询计划作为输入,然后使用local host的资源执行。Velox虽然不提供全局优化器,但是它执行时应用了大量的自适应(adaptive)技术,比如filter,conjunct reordering, dynamic filter pushdown和adaptive column prefetching等,总而言之它主要关注数据面(data-plane),上层引擎(Presto,Spark等)主要负责控制面(control-plane)。Velox在以下方面提供了优势:

  • 通过普及以前仅存在于个别引擎中的优化,提高效率
  • 为数据用户提供更一致的体验
  • 通过促进可重用性提高工程效率。

3. Velox Structure

3.1 边界:宿主、执行层与存储

层次 主要责任 当前代码入口
宿主系统 SQL/其他语言前端、优化、集群调度、协议、query 生命周期 Presto、Spark 等系统的集成代码
Velox core / exec 计划描述、Task、Driver、Operator PlanNode、QueryCtx、LocalPlanner
表达式与函数 批量求值、编码适配、函数语义 ExprSet、VectorFunction、函数注册
connector / DWIO 表与 split 抽象、文件格式读取、过滤下推 Connector::DataSource、TableScan
资源与数据表示 类型、向量、buffer、pool、arbitration、spill type / vector / common/memory

Velox 提供计划和执行接口,不要求每个使用者启动一个分布式 Worker。表达式可以独立求值,Task 也有串行与并行执行方式。仓库里的 parser、示例和测试 PlanBuilder 很有用,但不能据此把 Velox 描述成一个完整的生产 SQL coordinator。参见 QueryCtx 的使用场景、Task::create。

  • Type:描述标量、ARRAY、MAP、ROW 等复杂与嵌套类型,并携带参数、字段名和子类型。论文里的 tensor 场景需要用数组、自定义类型或宿主约定具体表达,不能把 tensor 当作当前通用内建 TypeKind。
  • Vector Arrow-compatible列式内存布局模块,支持多种编码,包括Flat,Dictionary,Constant,Sequence/RLE, and Bias等,还支持延迟物化,乱序写入。

  • Expression Eval 基于向量化编码(Vector-encoded)数据构建的,完全向量化的表达式求值引擎(expression evaluation engine),它借助了common subexpression elimination,constant folding,effective null propagation,encoding-aware evaluation和dictionary memoization等技术。

  • Functions Velox函数库提供了与流行的 SQL 方言兼容的函数包(目前,用于 Presto 和 Spark),还提供了简单(row-by-row)和向量化(batch-by-batch),聚合函数API,让开发人员构建自定义函数,库还提供了与流行SQL方言兼容的函数包(目前支持Spark和Presto)。这里论文没细说,虽然简单函数提供row-by-row的写法,最终还是向量化执行的,通过SimpleFunctionAdapter转换成向量函数,后面有空写一篇文章专门介绍。

  • Operator 实现了常用的数据处理的算子,包括TableScan,Project,Filter,Aggregation,Exchange/Merge,OrderBy,HashJoin,MergeJoin,Unnest等。

  • I/O 通用的connector interface,允许可插拔文件格式编码器/解码器和存储适配器。支持常见的格式,例如ORC,Parquet,以及 S3,HDFS等存储系统。

  • Serializers 网络通信的序列化接口,可以实现不同的有线协议,支持PrestoPage和Spark的UnsafeRow 格式。

  • Resource Management 用于处理计算资源的原语集合,例如内存区域(memory arenas),缓冲区管理(buffer managment),tasks,drivers(这里的driver指velox的pipeline的driver,非spark driver),线程池,spilling和cache。

4. 一批数据怎样经过统一执行层

用一个具体执行片段连接上面的组件:输入 (k,v)=[(A,3),(A,8),(B,6)],计算 v>5 后按 k 求 sum(v)。宿主负责生成有类型的 Filter、Aggregation 等计划节点,并提供 connector、executor 与 QueryCtx;Velox 执行后得到 (A,8)、(B,6)。这是接口教学例子,不表示所有宿主都会生成同一分布式计划。

执行阶段参与对象本例状态
计划进入宿主 → PlanFragment → Task → LocalPlanner把可连续传递批次的节点组织为 pipeline,创建 Driver 与 Operator。
获得输入TableScan → DataSource / DWIO → RowVector逻辑 schema 描述 k、v;Vector 携带三行值及编码,文件表示在 reader 边界转换。
过滤求值FilterProject → ExprSet → VectorFunction / SimpleFunction adapter选中输入 rows=[1,2];函数访问 C++ 值,选择集和结果仍属于 Vector 协议。
积累状态HashAggregation → GroupingSet → HashTable / RowContainer定位两个 group,聚合状态更新为 A:8、B:6;索引与 accumulator 职责分开。
等待或回收Driver isBlocked / Task pause / MemoryReclaimer外部事件等待使用 future 续调;内存压力按算子允许的阶段回收,必要时 spill 并恢复。
输出与结束getOutput → sink → consumer / serializer交付逻辑类型确定的结果,随后结束与清理仍受缓冲、异步引用和所有权协议约束。

过滤可能下推到 connector;这里把它放在 FilterProject,是为了明确执行层的协作。当下推成功,少进入后续计算的行是收益,但它没有改变表达式必须保持同样语义的要求。源码入口:FilterProject、Driver、GroupingSet。

5. Velox Deep Dive

5.1 Type

Velox支持标量类型和复杂类型,覆盖了基本上所有Presto和Spark的数据类型。在其核心,Velox提供了一个类型系统,允许用户表示原始类型,包括不同精度的整数和浮点数,字符串(包括varchar和varbinary类型),日期,时间戳和函数(lambdas)。它还支持复杂类型,如数组,固定大小的数组(用于实现ML张量),映射,以及行/结构;所有这些类型都可以任意嵌套,并提供序列化/反序列化方法。最后,Velox提供了一个不透明的数据类型,开发人员可以使用它来轻松包装任意的C++数据结构。 该类型系统是可扩展的,允许开发人员添加特定于引擎的类型,而无需修改主库。例如,Presto的HyperLogLog5类型用于基数估计,以及其他Presto特定的日期/时间数据类型,如带时区的时间戳。然后,可以在构建自定义标量和聚合函数时使用通过类型扩展性添加的类型。

5.1.1 Scalar Type

Velox中的标量类型是逻辑的并且与SQL兼容。每个标量类型都是使用C++类型实现的。下表显示了支持的标量类型及其对应的 C++ 类型。

Velox Type C++ Type Bytes per Value
BOOLEAN bool 0.125 (i.e. 1 bit)
TINYINT int8_t 1
SMALLINT int16_t 2
INTEGER int32_t 4
BIGINT int64_t 8
DATE struct Date 8
REAL float 4
DOUBLE double 8
SHORT_DECIMAL struct UnscaledShortDecimal 8
LONG_DECIMAL struct UnscaledLongDecimal 16
TIMESTAMP struct Timestamp 16
INTERVAL DAY TO SECOND struct IntervalDayTime 8
VARCHAR struct StringView 16
VARBINARY struct StringView 16

5.1.2 Complex Type

Velox还支持复杂类型,包括arrays,fixed-size arrays(用在ML tensor),maps,rows/structus;所有这些类型都可以任意嵌套并提供序列化/反序列化方法。

5.2 Vector

5.3 数据平面:类型与编码一起决定成本

Type 说明数据是什么,Vector 说明一批数据如何表示。一个 RowVector 通常把多个列向量组织成一批行;各子列可以使用不同编码,没必要全部是连续的 flat values。

编码/结构 作用 成本与边界
FlatVector 直接存一列值 读取简单;变长值还涉及外部 buffer
ConstantVector 多行共享一个值 避免重复存储与重复求值
DictionaryVector 索引映射到 base vector 可复用数据;增加间接访问和索引内存
LazyVector 延迟加载需要的数据 能节省读取;加载时机和生命周期更复杂
Array/Map/RowVector 组织嵌套数据 需同时处理父级 null、子级 null 与 offsets

DecodedVector 帮助执行代码处理多层编码、索引与 null;它不是“总先把输入完整解压成平面数组”的接口。Dictionary 可以让过滤、重分区等操作复用底层列,但索引、输出对象和必要的加载仍会分配内存。

类型也不能只看 TypeKind。例如 DATE 基于 INTEGER,DECIMAL 基于 BIGINT/HUGEINT,逻辑参数与比较规则要保留。详见 Type System。

Velox向量允许开发者利用各种编码格式在内存中表示列式数据集,并被用作大多数其他组件的输入和输出。基本的内存布局扩展了Apache Arrow格式,由size变量(表示向量中表示的行数)、数据类型以及一个可选的空值位图组成,用于表示空值。基础向量类还提供了一系列方法,帮助用户复制、调整大小、哈希、比较和打印向量。 向量可以表示固定大小(例如,primitive types like integers and floats)或可变大小的元素(例如,strings, arrays, maps, and structs/rows)。

row-vector:根据原图完整重绘,正文说明当前语义

Vectors也可以以任意方式嵌套(例如,包含字符串和其他原始类型的结构的数组的数组),并可以利用不同的编码格式,如flat, dictionary, constant, sequence/RLE, 和bias (frame of reference)。所有向量数据都使用Velox缓冲区存储,这些缓冲区是从内存池分配的连续内存片段,可以进行子类化以支持不同的所有权模式(例如,拥有和缓冲区视图)。所有向量和缓冲区都是引用计数的,一个单独的缓冲区可以被多个向量引用;自然,只有单一引用的数据是可变的,但任何向量和缓冲区都可以通过写时复制变得可写。

源码核对:唯一引用是可写性的重要条件之一;view、外部只读 buffer 和子向量仍要检查。ensureWritable 等接口按选中行与类型准备可写输出,不是任意裸写都会自动触发透明 COW。

此外,Velox提供了Lazy Vectors的概念,这些向量只在首次使用时填充。Lazy Vectors在如连接和投影中的条件等基数减少操作中非常有用,其中,根据操作的选择性,可以完全避免物化,或将其限制在少数有效的行中。当从远程存储(如S3或HDFS)读取向量数据时,这个特性特别有用,因为它可以为稀疏访问的列优化掉整个IO操作。Lazy Vectors还提供了对加载数据运行回调的支持,这可以用于在不必物化中间向量的情况下推下计算(如聚合)。

经常的,开发者无法控制特定向量的创建方式,例如在实现标量函数或运算符时,因此需要处理可能被任意编码的输入数据。一方面,这为开发者提供了利用输入数据编码进行高效处理的灵活性(例如,只对字典编码输入的不同值进行特定操作),另一方面,这增加了复杂性,并增加了开发者的认知负担。为了解决这个问题,Velox还提供了解码向量抽象,它将任意编码的向量转换为一个flat vector和一组索引,用于所有或部分元素,并提供一个逻辑一致的API。解码向量对于Flat、常量和单级字典编码输入(最常见的情况)是零复制的,但需要实现一个新的字典索引数组来覆盖多个字典/运行长度编码的嵌套。

尽管Velox向量基于并兼容Apache Arrow格式,但Velox向量和Apache Arrow格式在三个区域有所不同:

  • 字符串

Arrow使用传统的variable-sized elements布局来表示字符串,这包括一个包含字符串内容的缓冲区,以及一个表示字符串大小的长度缓冲区或一个标记字符串开始位置的偏移缓冲区,但在Velox的布局中,字符串向量也由两个缓冲区组成,一个用于元数据,每个字符串元素包含16字节,另一个用于存储字符串的数据。字符串元数据类被称为StringView,定义如下:

流程化代码节选:省略外围声明与非主线分支;实现位置以相邻固定版本源码链接为准。

strcut StringView {
    uint32_t size_;
    char prefix_[4];
    union {
        char inlined[8];
        const char* data;
    }
}

string-vector:根据原图完整重绘,正文说明当前语义

StringViews总是内联存储一个小的(4字节)前缀,专注于短路失败的比较(short-circuiting failed comparisons)以加速诸如过滤和排序等操作。此外,最大为12字节的小字符串完全内联,不需要访问次级缓冲区。这种布局还允许某些字符串操作,如𝑡𝑟𝑖𝑚()和𝑠𝑢𝑏𝑠𝑡𝑟(),通过仅更新元数据指针来执行零复制。

  • 支持乱序写入 为了有效地支持条件语句的执行,如IF和SWITCH操作,Velox扩展了Apache Arrow格式以支持乱序写入。在这些转换中,首先求值条件(Evaluation Condition)以生成一个位掩码,描述每行应采取哪个分支。随后,基于生成的位掩码,每个分支以向量化的方式单独处理,将计算出的值写入单个输出向量。原始类型总是可以乱序写入,因为元素大小是常数。此外,使用上述表示法,字符串也可以乱序写入,因为字符串元数据对象的大小是常数(16字节)。为了支持剩余的可变大小类型(如数组和映射)的乱序写入,Velox同时维护长度和偏移缓冲区。除了加速条件语句的执行,这种布局还为引擎提供了更多的灵活性,可以在不复制的情况下切片和重新排列元素,因为每个数组/映射的长度和偏移可以独立更新,并且可以表示引用重叠 child 范围的数组/映射;共享后的写入仍须遵守可写性与所有权协议。

  • 更多编码 Velox向量还添加了在数据仓库工作负载中常见的两种编码格式:游程长度编码(RLE)和常量编码。后者用于表示列中的所有值都相同,例如,表示文字和分区键。

Velox vector对scalar type和complex type的物理布局本质是一样的,都包含一个value buffer连续存放实际的value,null buffer标识value是否为null,比如下面的FlatVector与ArrayVector。

源码核对:FlatVector 的固定宽度 values 与 ArrayVector 的 offsets/sizes/child 是不同物理布局。ArrayVector 的父级 null 与 child null 也分开,不能把复杂类型理解为和标量一样只有一块连续 value buffer。ROW 则由 child vectors 表达字段,下面的图分别展示这些差异。

flat-vector:根据原图完整重绘,正文说明当前语义
array-out-of-order:根据原图完整重绘,正文说明当前语义

5.4 Expression

Velox的expression evaluation引擎可以被用在3种场景:

  1. FilterProject算子中过滤(filter)与投影(project)的表达式求值
  2. TableScan算子和IO connectors一致性求值谓词下推
  3. 独立的组件给只需要表达式求值功能的引擎,比如实时计算,ML场景的数据预处理

表达式求值的输入表达式树,有以下几种节点:

  • a reference to an input column,代表一个input RowVector的列(比如,C0),必定是叶子节点
  • a constant (or literal),必定是叶子节点
  • a function call,比如array_has_duplicates, hmac_sha256,还有类似AND/OR,IF/SWITCH,try这样的
  • a CAST expression
  • a lambda function,比(x, y) -> x + y

表达式树节点还包含以下两类元数据

  • 子表达式是否具有确定性,即相同的输入产出相同的结果
  • null传播,任何一个输入列的value为null是否总是让该表达式结果为null

5.4.1 Expression Trees

5.5 表达式:按选中行求值,利用编码和共享结果

表达式求值接收一组输入向量和选中行集合。SelectivityVector 表示哪些逻辑行需要计算;ExprSet 管理一组已构造的执行表达式。以 f(a) + f(a) 为例,可共享的子表达式不必机械地重复计算,但共享和缓存需要满足表达式确定性、输入映射、已求值行等条件。

图 2:选中行、编码和 null 参与表达式执行;优化必须保留函数语义。
图 2:选中行、编码和 null 参与表达式执行;优化必须保留函数语义。

Expr 的求值入口、编码 peeling、共享结果与 memoization 体现了几类优化:

  • 对可剥离的 constant/dictionary 包装,在底层值上求值,再恢复输出映射。
  • 对默认 null 传播的函数,减少无需计算的行。
  • 复用公共子表达式和适用的 dictionary memoization。
  • 为 flat、无 null 等常见情况选择更直接的执行路径。

这些机制都有适用条件。非确定函数不能被当成普通常量表达式缓存;TRY、条件分支和错误传播也不能被“全量先计算再过滤”随意改写。

函数既可以通过 simple function 接口实现逐行语义并由框架适配,也可以实现 vector function 直接处理选中行和向量编码。函数注册、参数签名和实现属于独立扩展点;同名函数在不同宿主语义下不一定可直接互换。

表达式求值以表达式树作为输入。树中的每个节点都是core::ITypedExpr的子类,它指定了返回类型和零个或多个输入表达式(树中的子节点)。每个表达式可以是以下之一:

  • FieldAccessTypedExpr(字段访问表达式)
  • ConstantTypedExpr(常量表达式)
  • CallTypedExpr(函数调用表达式)
  • CastTypedExpr(类型转换表达式)
  • LambdaTypedExpr(Lambda表达式)

FieldAccessTypedExpr表示输入RowVector的一列。该列由名称标识。这始终是树中的叶节点。 ConstantTypedExpr表示一个常量值(或字面值)。这始终是树中的叶节点。 CallTypedExpr表示一个函数调用。函数由名称标识。输入表达式指定函数的参数数量和类型,从而可以明确地识别特定的函数实现。该函数可以是简单函数或矢量化函数。 CallTypedExpr还可以通过指定预定义名称来表示特殊形式。这些名称不能被简单函数或矢量函数使用。

5.5.1 Compilation

将expression tree作为输入,编译成可执行的expression(Compiled Expression / Executable Expression)

  • 表达式消除(Common Subexpression Elimination) 比如strpos(upper (a), ‘FOO’) > 0 OR strpos(upper(a), ‘BAR’) > 0中的upper(a)只计算一次即可
  • 常量折叠(Constant Folding) 比如upper(a) > upper(‘Foo’)中upper(‘Foo’)是确定的,不依赖任何输入列,可以直接转成‘FOO’
  • 自适应重排(Adaptive Conjunct Reordering) 在求值AND或OR表达式时,引擎动态跟踪各个合取式的性能,并选择先评估最有效的合取式,即在最短时间内丢弃最多值的合取式,按照𝑡𝑖𝑚𝑒/(1 + 𝑛_𝑖𝑛 − 𝑛_𝑜𝑢𝑡)计算得分,得分越低越好。为了在执行过程中最大化自适应合取式重排序的效果,表达式编译还会将相邻的AND/OR表达式展平。例如,输入表达式AND(AND(AND(a, b), c), AND(d, e))在编译过程中被展平为单个AND(a, b, c, d, e)节点。

要编译一个表达式,需要创建一个exec::ExprSet的实例。ExprSet的构造函数接受一个表达式列表(core::ITypedExpr指向表达式树的根节点)和一个上下文(core::ExecCtx)。构造函数处理这些表达式,并创建exec::Expr类实例的树。ExprSet接受多个表达式,并识别出所有表达式中的公共子表达式,以便可以只计算一次。FilterProject运算符受益于这种能力,因为它为所有的过滤和投影表达式创建一个单独的ExprSet。编译步骤还会展开相邻的AND、OR和concat-line表达式,并执行常量折叠。表达式树中的每个节点都被转换为exec::Expr类的相应实例。

core::ITypedExpr node exec::Expr instance
FieldAccessTypedExpr FieldReference
ConstantTypedExpr ConstantExpr
CallTypedExpr - CastExpr if function name is “cast”;
- ConjunctExpr if function name is “and” or “or”;
- SwitchExpr if function name is “if” or “switch”;
- CoalesceExpr if function name is “coalesce”
- TryExpr if function name is “try”;
- Expr if function name is none of the above.
CastTypedExpr CastExpr
LambdaTypedExpr LambdaExpr

CallTypedExpr节点被处理以确定函数名称是否指向特殊形式表达式或函数(矢量化或简单函数)。查找按照以下顺序进行,并在第一个匹配时停止搜索:

  • 检查名称是否与special forms之一匹配
  • 检查名称和签名(即输入类型)是否与向量化函数之一匹配
  • 检查名称和签名(即输入类型)是否与简单函数之一匹配

5.5.2 Evaluation

求值过程接受一个编译的表达式和一个输入数据集(使用Velox向量表示),在计算结果后返回一个输出数据集。该过程包括对表达式树进行递归下降,传递一个行掩码,用于标识活动的(非空且未被条件掩码屏蔽)元素。在每个步骤中,可以避免评估两种情况:

  • 如果当前节点是一个常见的子表达式并且结果已经计算过(Common Subexpression Elimination)
  • 如果表达式被标记为传播空值,并且其任何输入为空。后一步可以通过简单地组合所有输入的空值位掩码并使用SIMD操作更新活动行掩码来高效实现(nulls propagation)

Peeling(剥离):当输入是字典编码时,可以通过仅考虑不同的值来高效计算确定性表达式。首先,需要验证所有输入列是否共享相同的字典封装,如果是,则剥离这些封装以提取内部向量集合(不同的值),在这些内部向量上评估表达式,并使用原始封装将结果重新封装回字典向量。例如,考虑一个使用字典编码的向量,表示颜色列的1k行数据集,使用包含3个值的字典进行编码:0 - red, 1 - green, 2 - blue。内存布局包括一个包含1k个值的索引缓冲区,范围为[0, 2],以及一个大小为3的内部向量,包含以下值:[red, green, blue]。例如,在评估表达式𝑢𝑝𝑝𝑒𝑟(𝑐𝑜𝑙𝑜𝑟)时,剥离字典封装后,upper函数仅应用于3个不同的值 - [红色,绿色,蓝色] - 生成另一个大小为3的向量:[RED,GREEN,BLUE]。作为最后一步,使用原始索引将结果封装为字典向量,生成一个表示1k个大写颜色值的字典编码向量。

Memoization(记忆化):评估步骤可以根据需要重复执行,以处理多个批次的数据,并重用相同的编译表达式对象。例如,当从TableScan运算符读取多个数据批次时,批次通常是字典编码的,并引用相同的基础向量。在上述描述的示例中,颜色列可能有数百万行引用相同的不同值基础集合[red, green, blue],由具有相同基础向量但不同索引缓冲区的字典编码向量表示。评估引擎利用这个特性,并记住在基础内部向量上计算的表达式评估结果,以便在后续批次中重用这些结果。对于每个新的批次,它只需使用输入向量的索引缓冲区包装现有的计算结果。

对原始数值的简单算术,peeling、memoization 等机制的额外组织成本可能抵消部分收益;字符串、正则与嵌套值函数则可能因避免重复解码和求值受益更多。论文给出了当时工作负载的观察,但具体优化仍取决于确定性、编码、选择行与函数成本,不能由机制本身承诺所有复杂表达式都显著加速。

将可执行的expression和input dataset计算结果,返回output dataset。这个过程包括递归下降处理expression tree,向下传递一个行掩码(row mask)标识有效的行。每个步骤都会避免重复计算

  • common subexpression
  • nulls传递expression,且有输入为null

Velox的开发者文档对Expression的优化描述更详细,可以对照看一下。

expression-evaluation:根据原图完整重绘,正文说明当前语义

5.5.3 Code Generation

Velox还提供了对通过代码生成(codegen)进行表达式求值的实验性支持。启用时,执行时将整个表达式树重写为C++函数的源代码,并将其写入源文件,使用常规编译器(如gcc或clang)将其编译为共享库。然后,将共享库动态链接到主进程,并在求值时使用它,而不是使用向量化的解释路径。考虑到代码生成过程涉及完整的编译器调用,编译时间通常很长(在某些情况下长达10秒),并且不适用于短期查询或交互式工作负载。相反,我们最初的评估重点是大型ETL查询(执行时间为几小时到几天),以及表达式树固定的用例。截至目前,Velox中的代码生成支持仍处于实验阶段。权衡编译延迟、降低开发人员生产力和可调试性,以及在向量化和基于代码生成的评估路径之间探索运行时适应性的工作是未来研究的开放问题和方向。

源码核对:本段完整保留早期论文的实验设计与取舍。当前快照不能据此推断默认表达式路径会调用系统编译器;常规执行仍应从 ExprSet/Expr 与函数实现追踪。实验能力的存在、维护状态和启用方式需要针对具体版本单独核对。

5.6 Function

Velox提供了API让开发者定制(主要是velox社区实现各类prestosql或者sparksql的functions,aggregate functions),scalar functions和aggregate functions。作为一个向量化引擎,Velox的scalar function API可以是向量化的,即输入参数是batch-by-batch的vectors,还包括 nullability buffers,active rows(bitmap),并可以利用vectors内置的null buffer,length buffer和key buffer等实现常量时间复杂度的is_null(), cardinality()和keys()等操作。

Simple Function Framework 让开发者以逐行调用形式实现函数,由框架负责批量遍历、输入解码、null 处理与输出写入。声明中的类型 tag 通过 resolver 映射为 C++ 输入视图 / 输出 writer:例如 VARCHAR 输入使用 StringView,ARRAY 输入使用 ArrayView,输出使用相应 Writer,并不是把所有非标量值先转换成 std::string / std::vector。输入视图可避免某些额外物化,但输出增长和变长数据写入仍可能分配或复制;框架也不承诺自动把任意 scalar 函数变成 SIMD 指令。ASCII 专用路径则是满足输入和函数语义条件时的另一类特化。

simple-function:根据原图完整重绘,正文说明当前语义

5.7 Operators

5.8 控制平面:计划变成 Pipeline、Driver、Operator

LocalPlanner 遍历 PlanNode,把不能在同一个流水线上顺序推进的边界拆开,形成 DriverFactory。每个 factory 描述一条 pipeline;运行时创建一个或多个 Driver,每个 Driver 拥有自己的 Operator 实例。参见 计划拆分、Driver 实例化。

Pipeline 是拓扑结构,Driver 是运行实例,executor thread 是执行它的线程。Driver 遇到等待输入、输出背压或 join build 未完成时,可以保存 future 并让出 executor,之后由回调重新调度;不能把 Driver 数直接当成常驻线程数。

算子之间通过 needsInput / addInput / getOutput / noMoreInput / isBlocked / isFinished 协作。getOutput() == nullptr 可能只是暂时没有输出;真正结束还需结合算子状态。HashBuild 就是一个通过 bridge 发布状态而不从 getOutput 返回行的 sink。

Hash join、local exchange、remote exchange 使用不同的协调结构。详细生命周期见 Task & Driver,入门例子见 Primer。

Velox查询计划由一棵PlanNode树组成,例如Filter、Project、TableScan、Aggregation、HashJoin、Exchange等,描述了要执行的计算过程。为了执行查询计划,计划节点首先被转换为运算符(Operator)。转换通常是一对一的,但也有一些例外,例如(a)Filter节点后跟Project节点被合并为单个FilterProject运算符,以及(b)具有两个或多个子节点的计划节点被转换为多个运算符,例如,HashJoin节点被转换为一对运算符,HashProbe和HashBuild,详细转换参见这里。。

Velox的顶层执行概念是Task,它是分布式执行中的函数传输单元,对应于查询计划片段以及其运算符树。任务以TableScan或Exchange(shuffle)源作为输入开始,并以另一个Exchange结束。任务的运算符树被分解为一个或多个线性子树,称为Pipeline:例如,HashProbe和HashBuild分别映射到一个Pipeline。每个Pipeline具有一个或多个执行线程,称为Driver,每个Driver都有自己的状态。Driver可以在线程上运行或不运行,这取决于它们是否有工作要执行。Driver之所以可能让出线程,有很多原因,例如,因为其消费者尚未消费数据,其源Exhange尚未产生数据,或者Scan正在等待文件扫描。与传统的Volcano迭代器树模型相比,这种模型更方便进行线程上下文切换,因为状态是可恢复的,无需在堆栈上构建控制流。最后,任务可以随时被其他Velox执行器取消或暂停。能够暂停任务在强制执行优先级、检查点状态、强制另一个任务溢出或其他协调活动的情况下非常方便。

源码核对:Driver 是一条 pipeline 的执行实例,持有独立 Operator 状态;它可在 executor 线程间迁移,不能直接等同 OS 线程。Task 输入输出也不必都为 Exchange:Values、connector source、结果 consumer 等都可构成边界。普通 pause/future 等待与仲裁保栈 suspended 是不同路径。

所有运算符实现了相同的基本API,包括添加一批向量作为输入、获取一批向量作为输出、检查运算符是否准备好接受更多输入数据以及通知不再添加数据的方法;后者可以用于通知阻塞排序或聚合刷新其内部状态并开始生成输出。尽管Velox已经提供了一套广泛使用的运算符,但该库还允许引擎开发人员添加包含特定于引擎的业务逻辑的自定义运算符,例如基于流的流处理聚合。

task-join-plan:根据原图完整重绘,正文说明当前语义

5.8.1 Table Scans, Filter, and Project

5.9 Connector、过滤下推与 Exchange

TableScan 从 Task 获取 split,把 connector-specific split 交给 DataSource,后者产出 RowVector。DataSource 的 next 接口可以返回数据、split 结束,或表示异步等待;调用方必须区分这些状态。

过滤下推可能在元数据、row group/stripe、解码和向量表达式等不同阶段发生。某个谓词能够转成 Filter,不等于每种 connector 都支持相同的下推,更不等于 residual filter 可以无条件删掉。Hash join 的 dynamic filter 同样受 join 类型、hash mode、spill 状态和上游能力限制。

数据交换也有两类:

  • Task 内 local exchange 传递 RowVector 和索引映射,管理队列与背压。
  • Task 间交换通常需要序列化、传输、反序列化。Velox 定义 ExchangeSource 等接口;Presto 的 HTTP 实现属于宿主。

当前常规交换客户端名为 InMemoryExchangeClient;InMemory 描述这一缓冲/交换实现,并不表示数据只能来自同一进程。网络来源由具体 ExchangeSource 实现。

表扫描是按列进行的,并支持过滤器下推。首先处理包含Filters的列,生成命中的行号以及可选的命中值。过滤器在运行时自适应排序,以便首先评估最小时间丢弃值的过滤器。得分定义为𝑡𝑖𝑚𝑒/(1 + 𝑛_𝑖𝑛 − 𝑛_𝑜𝑢𝑡),以使最优Filter在最短时间内丢弃最多的值。这与在前面的AND/OR表达式中重新排序连接词的原理相同。

简单过滤器使用SIMD一次评估多个值,这使得Velox能够使用AVX2在每个CPU时钟大约处理一个整数命中。对于字典编码的数据,Filters结果被缓存,并再次使用SIMD来使用gather + compare + mask lookup + permute检查缓存命中,以写出通过的行,平均每个CPU时钟处理多个命中。Velox还为大型IN Filters提供了高效的实现,用于哈希连接下推,它可以一次触发4个缓存未命中(4 * 64 = 256 = avx2_regsiter_width)。

源码核对:这里的“每周期”是早期介绍中的性能语境,不是所有 CPU/数据上的保证。AVX2 的 256-bit 寄存器可容纳四个 int64 lane,但 lane 数并不保证四次 cache miss 同时在途;gather 访问、有效 mask、冲突和缓存层级仍决定实际耗时。

此外,FilterProject运算符对于所有filter和project表达式使用单个表达式evaluate context。对于每批输入数据,运算符首先在所有输入行上计算filter表达式,然后仅对通过filter的行子集执行project表达式。如果没有行通过filter,将完全跳过project表达式的求值。

5.9.1 Aggregate and Hash Joins

哈希连接与聚合共享 HashTable / VectorHasher 等基础设施。VectorHasher 分析 key 的范围和基数,尝试 value_id 编码;HashTable 再结合空间估计、编码可表示性及启发式选择 Array、NormalizedKey 或 Hash。紧凑范围可直接寻址,多 key 可在允许时编码为 64 位 normalized key,但也有尚可编码而基于启发式选择 Hash 的分支,因此这不是“所有优化都不可能才退回”的严格阶梯。进入 kHash 后当前实现不再切回其他模式。HashProbe 通过 hasher.getFilter 或 getBloomFilter 生成过滤对象,经 Driver 的下推路径传给上游;不是把整个 VectorHasher 对象交给 TableScan。不同 key 的地址计算、预取与比较可以交错执行以重叠访存,实际收益受冲突与内存系统限制。当前 payload 采用 RowContainer 行布局,便于访问同一匹配行的字段,同时保留 Vector 作为批量输入输出接口。

5.10 Memory Management

5.11 资源管理:内存回收会进入执行流程

QueryCtx 汇集 query 配置、pool、executor、spill executor 等资源。未传入 pool 时,Velox 可以创建 query root;宿主也可以传入带限制的 root。Task 和 Operator 在其下建立资源层次。

内存管理至少区分 used、reserved、capacity 与进程 RSS。MemoryArbitrator 调整 root pool 之间的容量;释放对象、释放预留和缩小 root capacity 是不同操作。要回收算子状态,reclaimer 可能暂停 Task 并触发 spill;这会与 Operator 的可回收区间、future 和生命周期相互作用。

图 3:回收从 pool 进入算子,spill 完成后才能释放对应状态;容量变化不是单纯的 malloc/free。
图 3:回收从 pool 进入算子,spill 完成后才能释放对应状态;容量变化不是单纯的 malloc/free。

Spill 也不只是“写文件”:HashJoin 需要分区匹配与恢复,Aggregation 需要保存可合并中间态,OrderBy 需要保留有序 run 并归并。达到限制、无法回收或恢复后仍放不下时,查询仍可能失败;支持 spill 不等于无限内存。

详见 Memory Pool and Arbitrator 和 Spiller。

Velox 用 MemoryPool 树记录资源归属、reservation 与用量,root capacity 是仲裁额度,allocator 提供实际分配并有自身限制。部分控制对象仍直接使用 C++ 堆,所以 pool 统计不等于进程全部内存。当前 MemoryManager 默认使用 MallocAllocator,启用 useMmapAllocator 才选择 mmap 分配器;页、size class 和应用层布局都可能产生碎片,不能承诺零碎片。显式 reservation 可为一个操作争取预算,却不会预先保证所有物理分配成功。SharedArbitrator 以注册的 root participant 比较额度与回收能力,再由 Reclaimer 沿树进入 Task 和算子;它不是任意运行 Task 的全局调度器。Task pause 使执行状态停稳,算子再按阶段和可回收区规则 spill,必要时按终止协议 abort;普通 pause 与保留栈的 suspended 等待不同。没有 spill 能力的算子仍可能通过复用现有资源继续,也可能因资源限制失败。宿主可以提供仲裁和回收实现,消费者也可在各自接口允许时调整缓冲策略,但这不是所有 Exchange 自动随压力缩容的保证。

源码核对:“零碎片化”和预留保证不能作绝对理解:allocator 页粒度、pool reservation 量化与活跃 buffer 都会产生未用空间;reserve 成功后也可能遇到物理分配失败。当前 query-root 仲裁、Task pause、两阶段 reclaim/shrink 见本节补充;不可回收算子、磁盘及超时约束仍可能导致查询失败。

5.11.1 Caching

在存储与计算分离的部署中,Velox 的 AsyncDataCache 及可选 SSD cache 可以减少重复远程读取;是否启用及覆盖哪些 reader 路径取决于配置和集成。缓存使用其容量管理与淘汰机制,并非所有 I/O buffer 都经缓存分配,也不是可以无限占用所有进程空闲内存。缓存按数据范围组织读取,底层仍受 allocator 的页、size class、pin 与回收粒度约束;mmap / madvise 能参与页管理,但不能保证任意大小混合分配完全无碎片。

源码核对:缓存受 allocator、cache 配置和淘汰策略约束,不是任意消费整机全部未分配 RAM 的权限。mmap/madvise 管理页映射与回收,也不能消除所有内部、页粒度或应用层碎片。

首先从分散的存储系统(如S3或HDFS)读取缓存的列,存储在RAM中以供首次使用,并最终持久化到本地SSD。此外,如果它们之间的间隙足够小(目前对于SSD约为20K,对于分散存储约为500K),通常会合并附近列的IO读取,以尽可能少的IO读取来服务邻近的读取。自然地,这利用了时间局部性的效果,使相关的列在SSD上一起缓存。

许多列式格式可以先读元数据定位列块,再安排范围读取,使 I/O 与解码 / 计算重叠;具体索引、压缩块、加密和 reader 能力仍因格式而异。缓存与预取在命中率较高的工作负载中可显著减少关键路径等待,但冷缓存、带宽饱和、错误预测或远程抖动仍会影响延迟。论文中交互分析主要由缓存服务的观察应理解为当时部署与工作负载的结果,不能当作当前所有远程扫描都没有 I/O 停顿的保证。

下表保留原资料中的 Meta 硬件与工作负载实验:26 核服务器、64GB 内存、2×2TB SSD,查询标量列并执行简单过滤或聚合,吞吐包含读取及解码 / 解压过程。按表内数值,RAM 8GB/s 约为 SSD 2–3GB/s 的 2.7–4 倍;SSD 则约为远程 700MB/s 的 2.9–4.3 倍。原文“约 3 倍、约 4 倍”分别比较相邻层级,不能解释成 RAM 仅比远程快 4 倍。这是历史环境数据,不是当前源码版本的复测结果。

RAM SSD Disaggregated
Read rate 8GB/s 2-3GB/s 700MB/s

6. EXPERIMENTAL RESULTS

以下保留早期论文实验与硬件环境,未在本机复测。表中最后一组为总 CPU time,其 speedup 按给出的原始数值重新计算;行标签 Q9 沿原表保留,与上段文字中的 Q19 不一致,不能把这处旧记录差异当成新的实验结论。

6.1 论文实验与当前性能如何区分

2022 年论文报告了 Prestissimo 相对当时 Presto Java 的显著收益,并分析了向量化、编码、缓存和执行库复用的价值。这些结果来自论文指定的硬件、数据、查询和软件版本;它们是历史实验,不是这个 2026 年代码快照在任意系统上的加速承诺。

评估当前 Velox 应固定宿主版本与 Velox 提交,记录数据格式、缓存状态、线程数、内存限制、spill、编译选项和正确性校验。再按 CPU 执行、读写、排队、交换、内存回收逐层定位瓶颈,不能把一个 SIMD 微基准的加速直接换算成端到端查询加速。

本文核对了当前核心接口和实现,保留论文作为设计背景;没有重跑论文实验。

以下是原论文的历史实验,不是本轮重新运行的 benchmark。使用Prestissimo进行端到端测试所获得的实验结果,将新的基于C++的Velox执行引擎与当前的Presto Java实现进行了比较。测试平台是一个由80个节点组成的集群,每个节点有64G RAM和2x2TB SSD设备。两个系统都启用了本地缓存,并从热缓存运行。数据集是一个3TB的TPC-H,以ORC格式存储,没有zstd压缩,lineitem和orders共同分区。查询公式是手写的,以便具有正确的连接树形状,所有选择性连接都集中在构建侧,连接是哈希连接。表2展示了选定的CPU绑定查询(Q1和Q6)以及shuffle/IO重型查询(Q13和Q19)的CPU和wall time。对于CPU bound的查询,Q1和Q6,Prestissimo提供了接近一个数量级的加速,现在的瓶颈是协调器分派工作的速度。对于那些shuffle数据的查询,Q13和Q19,新的瓶颈是shuffle延迟。可能的优化是在协调器上更好地处理元数据,更好地调整和消息大小在shuffle上,可能还有一些非常轻量级的编码以减少shuffle数据量。

Wall time (sec) CPU time (sec)
C++ Java Speedup C++ Java Speedup
Q1 5 42 8.4x 2211 14335 6.5x
Q6 1 9 9x 538 2018 3.8x
Q13 15 31 2x 5647 12322 2.2x
Q9 6 13 2.1x 1362 3483 2.6x

以下是原论文的历史实验,不是本轮重新运行的 benchmark。虽然TPC-H仍然是系统比较的有效数据点,但它并不能全面代表现代工作负载。为了评估Velox在实际工作负载下的性能,在Meta中找到的各种交互式分析工具生成的生产流量重播到两个具有相同硬件特性的集群(一个运行Prestissimo,一个运行Presto Java)。Prestissimo相对于Presto Java提供的相对加速的平均加速约为6-7倍,但许多查询观察到的加速超过一个数量级。

以下是原论文的历史实验,不是本轮重新运行的 benchmark。最后,除了最初的问题,即新的基于C++的堆栈可以节省多少CPU之外,当处理超大规模系统部署时,一个自然的后续问题是这个新堆栈在服务器数量方面的容量影响,这最终转化为数据中心的电力。为了进行这个实验,创建了两个集群(一个Prestissimo和一个Presto Java),它们完全复制了相同的生产工作负载,并慢慢减少了Prestissimo集群中的服务器数量。论文作者观察到,使用基于Velox的堆栈,Prestissimo能够以相等或更好的用户感知性能支持相同的工作负载,但服务器数量减少了3倍(从60减少到20)。

7. 统一执行库的动机

7.1 为什么需要统一执行库

多个数据系统都要实现扫描、过滤、聚合、join、排序和类型处理。如果每个系统单独维护,SIMD、编码适配、内存回收和算子边界条件会被重复解决。Velox 的价值在于把这些能力放到可组合的执行层,允许不同宿主共享改进,同时保留自己的语言、优化器、元数据和分布式调度。

2022 年论文 与 Meta 发布文章 描述了这一动机。它并不意味着接入 Velox 后所有系统自动具有相同 SQL 语义;函数集合、类型转换、null 规则以及计划转换仍要与宿主匹配。

现代数据工作负载的多样性在不断增加,数据集指数级增长,导致了专门的查询和计算引擎的泛滥,每个引擎针对特定类型的工作负载。数据处理需求从简单的事务处理和分析(批处理和交互式),发展到ETL和大规模数据移动,再到实时流处理,以及用于监控用例的日志和时间序列处理,最近还包括大量的人工智能(AI)和机器学习(ML)用例,包括数据预处理和特征工程。这种演变导致了一个由数十个专门引擎组成的孤立数据生态系统,这些引擎使用不同的框架和库构建,彼此之间几乎没有共享,使用不同的编程语言编写,并由不同的工程团队维护。

此外,随着硬件和用例的演进,对这些引擎进行演进和优化的成本是不可接受的,如果按照每个引擎的方式进行。例如,将每个引擎扩展以更好地利用新的硬件进展,如高速缓存一致性加速器和非易失性内存(NVRAM),支持ML工作负载的张量数据类型等功能,并利用研究界未来的创新是不切实际的,这必然会导致具有不同优化和功能集的引擎。更重要的是,这种碎片化最终影响了数据用户的生产力,他们通常需要与多个不同的引擎交互才能完成特定的任务。这些系统中可用的数据类型、函数和聚合物各不相同,这些函数的行为、空值处理和类型转换在不同的引擎之间可能存在巨大的不一致性。例如,在Meta进行的一项非正式调查中,至少发现了12种不同的简单字符串操作函数𝑠𝑢𝑏𝑠𝑡𝑟()的实现,它们具有不同的参数语义(基于0还是1的索引)、空值处理和异常行为。

尽管专门的引擎根据定义提供了能够证明其存在的专门行为,但主要的区别通常在于语言前端(SQL、数据框架和其他DSL)、优化器、任务在工作节点之间的分配方式(也称为运行时)以及IO层。这些系统核心的执行引擎都非常相似。Velox被用在Meta内部的各个不同的引擎,虽然它们在前端语言(SQL,dataframe等),优化器,分布式任务调度等方面都不完全相同,但是在执行引擎(execution engine)层面大致相同的,都需要一个类型系统来表达scalar和complex数据类型,一个内存数据集表示(基本都是columnar,类似的有Arrow),一个表达式求值系统(expression evaluation),算子(Operator,比如joins,aggregation,sort等),以及存储和网络序列化、编码格式和资源管理原语。

8. 从复用收益与集成成本评价统一执行

统一执行库的收益来自多个宿主共同复用数据表示、运算内核和执行机制,维护一份实现也有机会让修复与优化传播到多种系统。这个收益需要接口边界足够稳定,同时允许宿主表达自己的语义与运行环境。

选择 可以复用的部分 无法自动消除的差异
统一 Type / Vector 接口 批处理的数据表示、编码与访问路径 方言类型、文件表示及自定义比较规则
统一表达式和函数执行框架 解码、选择行、结果分配与调用适配 函数语义、错误行为及具体内核实现
统一 Task / Driver 执行 本地调度、等待和算子协作 全局计划、分布式调度、网络协议与任务管理
统一资源机制 pool、仲裁、reclaim 和 spill 的共性 算子能否回收、宿主容量策略与存储限制

“统一”要求抽象共同职责,“可扩展”要求差异能够通过明确接口进入系统。二者之间的张力会体现在类型特化、connector、函数注册和回收协议中。为所有宿主硬编码同一套假设会限制复用,完全不约束扩展又会使公共执行路径难以推理和优化。

这里值得品味的是数据表示与执行策略的协同:保留已有编码可以避免过早物化,向量接口提供批处理机会,函数和算子仍在必要位置使用具体类型与布局。抽象并不意味着每层都隐藏全部细节,而是决定哪些信息必须稳定传递、哪些优化由局部实现承担。

论文实验说明这种复用路径在给定场景下有价值;评估一次实际接入,还应计入计划转换、语义一致性、I/O、内存、错误处理与运维成本。端到端收益来自这些部分的共同结果,不能从某个向量内核的局部加速直接推出。

统一执行的复用单位是明确的执行契约,而不是让各宿主共用一切政策。函数库仍要选择相应 SQL 方言,connector 仍要处理表与文件语义,Task 需要宿主提供线程和资源配置。前面的批次例子说明共享的是类型、编码适配、算子执行与资源机制;论文实验反映特定集成与负载的结果,不能把组件复用本身推导成固定性能倍数。

9. 参考

9.1 一条可执行的阅读路线

  1. 从 Type 与 Vector 建立数据表示模型。
  2. 用 Primer 串起 PlanNode、Task、Pipeline 和 Driver。
  3. 读 Query in Presto Worker 理解分布式宿主边界。
  4. 读 HashTable 和 HashJoin 追踪数据与同步。
  5. 最后把 MemoryManager、Arbitrator 和 Spill 接回执行生命周期。

源代码入口:PlanNode、Expr、LocalPlanner、Connector。文章间链接帮助建立层次,具体行为与限制以固定提交中的实现为准。

源码核对(2026-09-20):本轮按 Velox 1d1b76567870 核对关键接口、控制流、默认值与边界条件。当前源码摘录附固定版本链接;流程伪代码用于说明分支,不是可直接编译的程序。未对全文示例做独立编译或性能复测。涉及宿主集成与历史实验的数据,按各节标注的来源理解。