1. 1. 一、模板元编程
    1. 1.1. 1.1 SFINAE 与 void_t 类型探测
    2. 1.2. 1.2 DECLARE_METHOD_RESOLVER + has_method — 编译期方法探测
    3. 1.3. 1.3 if constexpr — 编译期分支
    4. 1.4. 1.4 overloaded — std::variant 多类型访问
    5. 1.5. 1.5 非类型模板参数 (NTTP) 与”具名模板参数”技巧
    6. 1.6. 1.6 全特化 vs 偏特化 — 类模板的两种特化
    7. 1.7. 1.7 函数模板不能偏特化 — 一个关键限制及 Velox 的应对
  2. 2. 二、类型系统
    1. 2.1. 2.1 TypeKind 枚举 + TypeTraits 特化
    2. 2.2. 2.2 DECLARE_CONDITIONAL_TYPE_NAME — 可选类型别名
  3. 3. 三、内存管理
    1. 3.1. 3.1 MemoryPool — 层级内存池
    2. 3.2. 3.2 raw_vector<T> — 带池分配且 SIMD 友好的向量
    3. 3.3. 3.3 STL Allocator 适配 — 标准容器接入内存池
  4. 4. 四、错误处理
    1. 4.1. 4.1 分层异常体系 + [[noreturn]] 出错路径优化
    2. 4.2. 4.2 两类错误宏 — 系统错误 vs 用户错误
    3. 4.3. 4.3 VELOX_DCHECK — 仅在 Debug 模式有效的检查
  5. 5. 五、SIMD 与向量化
    1. 5.1. 5.1 xsimd — 跨平台 SIMD 抽象
    2. 5.2. 5.2 SelectivityVector — 位集合批处理
    3. 5.3. 5.3 AVX2 Intrinsics — 解码热路径
  6. 6. 六、性能工程
    1. 6.1. 6.1 FOLLY_ALWAYS_INLINE 与 FOLLY_LIKELY/UNLIKELY
    2. 6.2. 6.2 移动语义与完美转发
    3. 6.3. 6.3 constexpr 与编译期常量
  7. 7. 七、Folly 库深度集成
    1. 7.1. 7.1 folly::Synchronized<T> — 数据与锁绑定
    2. 7.2. 7.2 folly::F14FastMap/Set — SIMD 加速的哈希容器
    3. 7.3. 7.3 folly::Executor — 解耦执行引擎与线程调度
  8. 8. 八、设计模式
    1. 8.1. 8.1 Builder 模式 + std::optional 延迟校验
    2. 8.2. 8.2 Visitor 模式 — Plan Tree 遍历
    3. 8.3. 8.3 Strategy 模式 — Connector 插件体系
  9. 9. 九、向量化执行引擎
    1. 9.1. 9.1 多级编码 Vector 体系
    2. 9.2. 9.2 DecodedVector — 透明展平多级编码
    3. 9.3. 9.3 LazyVector — 延迟物化与 IO
  10. 10. 十、Arrow 零拷贝集成
    1. 10.1. 10.1 Arrow C Data Interface
  11. 11. 十一、并发设计
    1. 11.1. 11.1 原子变量 — 无锁计数器与状态机
    2. 11.2. 11.2 Task / Driver / Operator 无共享并发模型
  12. 12. 十二、CRTP — 静态多态与可复用 Builder
  13. 13. 十三、Policy-based Design — 编译期行为注入
  14. 14. 十四、StringView — 小字符串优化 (SSO) 与前缀缓存
  15. 15. 十五、folly::Expected<T, E> — 无异常错误传播
  16. 16. 十六、[[nodiscard]] — 强制检查返回值
  17. 17. 十七、folly::Range — 非拥有数组视图
  18. 18. 十八、结构化绑定 (Structured Bindings) — 多返回值
  19. 19. 十九、noexcept — 移动语义与性能保证
  20. 20. 二十、类型双关 (reinterpret_cast) — 位级操作
  21. 21. 二十一、constexpr 查找表 — 以空间换时间
  22. 22. 二十二、std::call_once / folly::once_flag — 线程安全单次初始化
  23. 23. 二十三、Template Template Parameters — 类型变换框架
  24. 24. 二十四、Hidden Friend Idiom — 限制 ADL 查找范围
  25. 25. 二十五、= delete 删除危险重载 — 主动防御
  26. 26. 二十六、条件变量 (condition_variable) — 等待状态变化
  27. 27. 二十七、folly Future / Promise — 异步值的生产与消费
  28. 28. 二十八、类的特殊成员函数设计 — Rule of Zero/Three/Five
    1. 28.1. 28.1 为什么禁止拷贝和移动 (= delete)
    2. 28.2. 28.2 为什么有的类不需要写析构函数
    3. 28.3. 28.3 为什么有的类必须定义虚析构函数
  29. 29. 二十九、shared_ptr / weak_ptr — 引用计数与打破循环
    1. 29.1. 29.1 两种计数器:强引用 vs 弱引用
    2. 29.2. 29.2 .lock() 的提升技巧:原子地”续命”
    3. 29.3. 29.3 Velox 应用:异步回调持有 weak_ptr,回调内 lock 续命
  30. 30. 三十、boost::intrusive_ptr — 侵入式引用计数
  31. 31. 小结

Velox C++实践

Velox 是 Meta 开源的高性能 C++ 数据执行引擎,服务于 Presto、Spark Native Execution 等场景。
代码库约 3800 个源文件,要求 C++20、GCC 11+ 或 Clang 15+。
本文从设计思路和代码技巧两个维度,梳理 Velox 用到的重要 C++ 特性。


一、模板元编程

1.1 SFINAE 与 void_t 类型探测

是什么:SFINAE(Substitution Failure Is Not An Error)是 C++ 模板的一个规则:
当编译器为某个模板做类型替换失败时,不报错,而是直接忽略这个候选。
利用这一规则,可以在编译期”探测”一个类型是否具有某种能力(成员、方法、关联类型),
从而在不修改该类型的前提下,对不同类型走不同的代码路径。
std::void_t(C++17)是一个辅助工具:它接受任意数量的类型参数,始终返回 void
只要括号里的表达式合法,特化就被选中;否则退回 fallback 版本。

解决什么问题:虚函数继承要求被探测的类型必须实现接口,且有运行时开销。
SFINAE 无需修改类型,探测在编译期完成,运行时零开销。
适合”可选接口”场景:有则用,无则走默认路径。

Velox 例子velox/core/Metaprogramming.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 探测 T 是否为 map-like 类型(有 key_type、mapped_type、operator[])
template <typename T, typename U = void>
struct is_mappish_impl : std::false_type {};

template <typename T>
struct is_mappish_impl<
T,
void_t<
typename T::key_type,
typename T::mapped_type,
decltype(std::declval<T&>()[std::declval<const typename T::key_type&>()])>>
: std::true_type {};

template <typename T>
struct is_mappish : detail::is_mappish_impl<T>::type {};

括号里三个表达式只要有一个不合法(比如 T 没有 key_type),void_t 整体替换失败,
编译器选择 false_type 版本,不报错。这是 Velox 类型系统用来区分容器类型与标量类型的基础。


1.2 DECLARE_METHOD_RESOLVER + has_method — 编译期方法探测

是什么:在 SFINAE 基础上进一步封装:定义一个”探测器”结构体,
再用 has_method<C, Resolver, RetType, Args...> 判断类型 C 是否拥有指定签名的方法。

解决什么问题:Velox 的 UDF(用户自定义函数)允许用户选择性地实现 callNullable()
initialize()destroy() 等可选方法。
如果用虚函数,所有 UDF 都要继承并实现一套接口,代价高。
用 has_method,框架在编译期探测 UDF 类是否有这些方法,有就调用,没有就跳过,
生成的代码与手写 if-else 等价。

Velox 例子velox/core/Metaprogramming.h:146

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 宏:生成一个"方法探测器"结构体
#define DECLARE_METHOD_RESOLVER(Name, MethodName) \
struct Name { \
template <class __T, typename... __TArgs> \
constexpr auto resolve(__TArgs&&... args) const \
-> decltype(std::declval<__T>().MethodName(args...)) { \
return {}; \
} \
}

// 实际判断:C 有没有返回 bool、接受 (int, int) 的 callNullable 方法?
DECLARE_METHOD_RESOLVER(CallNullableResolver, callNullable);
constexpr bool hasCallNullable =
has_method<MyUDF, CallNullableResolver, bool, int, int>::value;

UDF 适配层用这个机制在编译期选择调用路径,零运行时分支。


1.3 if constexpr — 编译期分支

是什么:C++17 引入的 if constexpr,让 if 的条件在编译期求值。
被丢弃的分支不会被实例化,也不要求语法正确(只要能解析)。
这和普通 if 的区别在于:普通 if 两个分支都要能通过类型检查,
if constexpr 只检查被选中的分支。

解决什么问题:在模板函数里,不同类型需要走完全不同的代码路径,
用普通 if 会因为某个分支对当前类型不合法而编译报错。
if constexpr 让模板函数同时处理多种类型,不需要写多个特化版本,代码更集中。

Velox 例子velox/core/SimpleFunctionMetadata.h:107

1
2
3
4
5
6
7
8
9
10
11
// 验证 Variadic 参数只能出现在参数列表末尾
template <int32_t POSITION>
static constexpr bool isValidArg() {
if constexpr (POSITION >= num_args - 1) {
return true; // 到达末尾,不再递归
} else {
// 当前位置不能是 Variadic,且后续也要合法
return !isVariadicType<arg_at<POSITION>>::value &&
isValidArg<POSITION + 1, Args...>();
}
}

另一个例子,velox/vector/DictionaryVector.h

1
2
3
4
5
6
7
8
9
10
bool containsNullAt(vector_size_t idx) const override {
if constexpr (std::is_same_v<T, ComplexType>) {
// 复杂类型:需要同时检查当前层的 null 和字典中的 null
if (isNullAt(idx)) return true;
return dictionaryValues_->containsNullAt(getDictionaryIndex(idx));
} else {
// 标量类型:只有当前层的 null
return isNullAt(idx);
}
}

单个函数处理 int64_tComplexType 两种完全不同的逻辑,编译器为每种类型生成最优代码。


1.4 overloaded — std::variant 多类型访问

是什么std::variant 是 C++17 的类型安全联合体,用 std::visit 访问。
overloaded 是一个常见惯用法:将多个 lambda 合并成一个重载集,每个 lambda 处理一种类型。
它利用了 C++17 的 pack expansion using Ts::operator()... 和 CTAD(类模板参数推导)。

解决什么问题:对 variant 做 std::visit 时,通常需要写一个 visitor 类,
为每种类型写一个 operator()overloaded 让你直接用 lambda 列表完成,
代码更紧凑,且类型不匹配时编译报错,比 if-else + std::get<> 安全。

Velox 例子velox/core/Metaprogramming.h:67

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 定义
template <class... Ts>
struct overloaded : Ts... {
using Ts::operator()...; // 把所有 lambda 的 operator() 都继承进来
};
template <class... Ts>
overloaded(Ts...) -> overloaded<Ts...>; // CTAD 推导指引,让编译器自动推类型

// 使用:对不同类型的 variant 值做不同处理
std::visit(overloaded{
[](int64_t v) { return fmt::format("bigint:{}", v); },
[](double v) { return fmt::format("double:{}", v); },
[](std::string_view v){ return fmt::format("varchar:{}", v); },
}, literalValue);

1.5 非类型模板参数 (NTTP) 与”具名模板参数”技巧

是什么:模板参数不一定是类型,也可以是——整数、枚举、bool、指针等,
称为非类型模板参数(Non-Type Template Parameter, NTTP)。
形式如 template <int32_t POSITION>template <bool mayHaveNulls>template <TypeKind Kind>
这些值在编译期就确定,编译器为每个不同的值生成一份独立的代码。

给 NTTP 起一个有意义的名字(而不是 template <int N> 里的 N),
是一种重要的可读性技巧:template <bool mayHaveNulls> 一眼就能看出这个开关控制什么,
比注释更可靠。

解决什么问题:热路径上常有”配置开关”——是否处理 null、是否是 join 模式、
是否用行号索引。如果用运行时 if (mayHaveNulls),每行数据都要判断一次,
且分支预测有成本。
把开关提升为 NTTP,编译器为”有 null”和”无 null”各生成一个特化版本,
每个版本内部的 if constexpr (mayHaveNulls) 在编译期就被消解,
运行时是两段没有分支的直线代码。这是”用代码膨胀换取分支消除”的经典权衡。

Velox 例子velox/exec/RowContainer.h:1025velox/exec/HashTable.h:1060

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 具名 bool NTTP:useRowNumbers 和 Kind 都是编译期常量
// 一个函数模板,编译器为每种 (useRowNumbers, Kind) 组合生成一份特化
template <bool useRowNumbers, TypeKind Kind>
static void extractColumnTypedInternal(
const char* const* rows,
folly::Range<const vector_size_t*> rowNumbers,
int32_t numRows,
RowColumn column,
bool columnHasNulls,
int32_t resultOffset,
const VectorPtr& result) {
// Kind 是编译期值,这个分支在编译期就决定了,不生成运行时判断
if constexpr (Kind == TypeKind::ROW || Kind == TypeKind::ARRAY ||
Kind == TypeKind::MAP) {
extractComplexType<useRowNumbers>(rows, rowNumbers, numRows, column, ...);
return;
}
// 标量路径:用 KindToFlatVector<Kind> 拿到具体 C++ 类型
using T = typename KindToFlatVector<Kind>::HashRowType;
// ...
}

// HashTable 探测:isJoin 和 isNormalizedKey 都是具名 bool NTTP
// groupProbe 和 joinProbe 共享同一份逻辑,但编译出两个无分支的版本
template <bool isJoin, bool isNormalizedKey = false>
void HashTable<ignoreNullKeys>::lookupRows(...) {
if constexpr (isJoin) {
// join 专用逻辑,group 模式下这段代码根本不存在
} else {
// group 专用逻辑
}
}

template <int32_t POSITION> 的递归用法见 1.3 节isValidArg——
POSITION 是编译期整数,每次递归 isValidArg<POSITION + 1>() 实例化一个新的特化,
直到 POSITION >= num_args - 1 终止。整个验证在编译期完成,运行时零代码。


1.6 全特化 vs 偏特化 — 类模板的两种特化

是什么:模板特化(Specialization)是为特定模板参数提供专门实现。分两种:

  • 全特化(Full / Explicit Specialization):所有模板参数都被指定具体值,
    写法 template <> struct Foo<int> {...}。它不再是模板,是一个具体类型。
  • 偏特化(Partial Specialization):只指定部分参数,或对参数施加结构约束
    (如”任意 shared_ptr<T>“),写法 template <typename T> struct Foo<shared_ptr<T>> {...}
    特化后仍然是模板(还剩 T 没定)。

解决什么问题:主模板提供”通用但可能无意义”的默认行为,
特化为特定类型提供正确/高效的实现。
全特化处理”某个确切类型”,偏特化处理”某一类符合某种结构的类型”。

Velox 例子velox/type/CppToType.h:24

CppToType<T> 把 C++ 类型反向映射到 Velox 的 TypeKind,是 UDF 模板系统的基石:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 主模板:空的,故意不可用——传入未知类型会编译报错(而非默默出错)
template <typename T>
struct CppToType {};

// 全特化:每个确切的 C++ 类型映射到一个 TypeKind
template <>
struct CppToType<int64_t> : public CppToTypeBase<TypeKind::BIGINT> {};
template <>
struct CppToType<int32_t> : public CppToTypeBase<TypeKind::INTEGER> {};
template <>
struct CppToType<bool> : public CppToTypeBase<TypeKind::BOOLEAN> {};
template <>
struct CppToType<velox::StringView> : public CppToTypeBase<TypeKind::VARCHAR> {};

// 偏特化:匹配"任意 shared_ptr<T>"——注意它仍带模板参数 T
// 这是全特化做不到的:你无法为"所有 shared_ptr"写无数个全特化
template <typename T>
struct CppToType<std::shared_ptr<T>> : public CppToTypeBase<TypeKind::OPAQUE> {
static auto create() {
return OpaqueType::create<T>(); // T 仍可用,转发给 OpaqueType
}
};

TypeTraits<TypeKind::BIGINT>(见 2.1 节)则是
对 NTTP 做全特化:主模板 template <TypeKind KIND> struct TypeTraits {} 是空的,
每个 TypeKind 枚举值都有一个 template <> struct TypeTraits<TypeKind::XXX> 全特化。

另一个偏特化例子——用 enable_if 对一类型做特化:velox/type/Type.h:701

1
2
3
4
5
6
7
8
9
10
11
12
// 主模板:默认所有类型都不支持自定义比较
template <TypeKind KIND, typename = void>
struct kindCanProvideCustomComparison : std::false_type {};

// 偏特化:对"原始类型且定宽"这一类 KIND,启用为 true
// SFINAE + 偏特化:只有满足 enable_if 条件的 KIND 才匹配这个特化
template <TypeKind KIND>
struct kindCanProvideCustomComparison<
KIND,
std::enable_if_t<
TypeTraits<KIND>::isPrimitiveType && TypeTraits<KIND>::isFixedWidth>>
: std::true_type {};

1.7 函数模板不能偏特化 — 一个关键限制及 Velox 的应对

是什么:这是 C++ 一个容易踩坑的规则——
类模板可以偏特化,但函数模板不能偏特化
你可以全特化一个函数模板(template <> void f<int>()),
但不能写 template <typename T> void f<std::vector<T>>()(偏特化),这是语法错误。

为什么有这个限制:函数有重载机制,而类没有。
C++ 标准委员会认为函数偏特化会与重载决议规则纠缠不清
(重载 + 偏特化的优先级难以定义清楚),因此干脆禁止函数偏特化,
让你用重载来达到同样目的。

应对方式(Velox 中都有体现)

方式一:函数重载 —— 最直接,用参数类型区分而非特化

1
2
3
4
// 不要试图偏特化,直接重载
void process(int32_t v);
void process(const StringView& v);
template <typename T> void process(const std::vector<T>& v); // 这是重载,不是偏特化

方式二:委托给类模板的静态方法 —— 类能偏特化,把逻辑搬进类

1
2
3
4
5
6
7
8
9
10
11
12
// 想"偏特化"一个函数?把它包成类模板的 static 方法
template <typename T>
struct Processor { // 主模板
static void run(const T& v) { /* 通用 */ }
};
template <typename T>
struct Processor<std::shared_ptr<T>> { // 偏特化类(合法!)
static void run(const std::shared_ptr<T>& v) { /* 专用 */ }
};
// 用一个普通函数模板转发,对外像个可偏特化的函数
template <typename T>
void process(const T& v) { Processor<T>::run(v); }

CppToType(1.6 节)正是这个模式:与其偏特化函数,不如偏特化 struct CppToType<shared_ptr<T>>

方式三:if constexpr 内部分发 —— 一个函数模板内用编译期分支代替多个特化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// velox/type/Type.h:2607 —— 一个 valueToString 处理所有类型,不写任何特化
template <typename T>
std::string Type::valueToString(T value) const {
if constexpr (std::is_same_v<T, bool>) {
return value ? "true" : "false";
} else if constexpr (std::is_same_v<T, int128_t>) {
if (isDecimal()) return LongDecimalType::toString(value, *this);
return velox::to<std::string>(value);
} else if constexpr (std::is_same_v<T, int32_t>) {
if (isDate()) return DATE()->toString(value);
return velox::to<std::string>(value);
} else {
return velox::to<std::string>(value); // 兜底
}
}

方式四:宏生成的运行时→编译期 dispatch —— 把运行时 TypeKind 转成编译期模板实参

这是 Velox 最核心的桥梁。一个函数模板按 TypeKind 参数化,但 TypeKind 是运行时值,
无法直接当模板实参。VELOX_DYNAMIC_TYPE_DISPATCH 宏用一个 switch 把运行时值
“翻译”成编译期模板实参:velox/type/Type.h:2197

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 宏展开成一个 switch,每个 case 实例化一份特定 Kind 的模板函数
#define VELOX_DYNAMIC_TYPE_DISPATCH(TEMPLATE_FUNC, typeKind, ...) \
[&]() { \
switch (typeKind) { \
case TypeKind::BIGINT: \
return TEMPLATE_FUNC<TypeKind::BIGINT>(__VA_ARGS__); \
case TypeKind::INTEGER: \
return TEMPLATE_FUNC<TypeKind::INTEGER>(__VA_ARGS__); \
/* ... 每种 TypeKind 一个 case ... */ \
} \
}()

// 使用:运行时拿到 type->kind(),dispatch 到编译期特化的模板函数
// 等效于"对函数做了运行时分发的偏特化"
auto result = VELOX_DYNAMIC_TYPE_DISPATCH(extractColumnTyped, type->kind(), rows, ...);

这个宏是连接”运行时动态类型 SQL”和”编译期零开销模板代码”的关键:
SQL 引擎在运行时才知道列类型,宏负责把这个运行时信息一次性转换为编译期的模板实例化,
之后所有操作都是类型特化的高效代码。


二、类型系统

2.1 TypeKind 枚举 + TypeTraits 特化

是什么:C++ 模板特化(Template Specialization)允许为特定类型参数提供完全不同的实现。
与枚举结合,可以把运行时的类型标签(TypeKind::BIGINT)映射到编译期的 C++ 类型(int64_t),
形成一个”双轨并行”的类型系统:枚举用于运行时 switch,模板特化用于编译期泛型代码。

解决什么问题:查询引擎需要在运行时处理未知类型,也需要在编译期为每种类型生成高效代码。
如果只有运行时类型,就全是虚函数和类型转换,性能差。
如果只有编译期类型,就无法处理用户的动态 SQL。
TypeKind + TypeTraits 的组合让两者共存:外层按 TypeKind 分发,内层是类型安全的模板代码。

Velox 例子velox/core/Type.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 每种 TypeKind 都有一个特化,描述它的 C++ 原生类型
template <>
struct TypeTraits<TypeKind::BIGINT> {
using ImplType = BigintType;
using NativeType = int64_t; // 存储和计算用这个类型
using DeepCopiedType = int64_t;
static constexpr bool isFixedWidth = true;
static constexpr bool isPrimitiveType = true;
};

template <>
struct TypeTraits<TypeKind::VARCHAR> {
using ImplType = VarcharType;
using NativeType = StringView; // 字符串用轻量视图,不拷贝数据
using DeepCopiedType = std::string;
static constexpr bool isFixedWidth = false;
static constexpr bool isPrimitiveType = true;
};

运行时拿到 TypeKind,用 VELOX_DYNAMIC_TYPE_DISPATCH(func, kind, args...) 宏展开成
一个 switch,每个 case 实例化对应的模板函数,从而把运行时类型转换为编译期类型。


2.2 DECLARE_CONDITIONAL_TYPE_NAME — 可选类型别名

是什么:SFINAE 的一个具体应用模式:检查类型 T 是否有内嵌类型 TypeName
有则用它,没有则退回默认的 OtherTypeName
这是实现”可选接口”的编译期版本。

解决什么问题:UDF 可以声明 return_type 来覆盖框架推断的返回类型,也可以不声明。
框架需要在编译期判断用哪个,且不能让 UDF 必须继承某个基类来声明这个类型。

Velox 例子velox/core/Metaprogramming.h:158

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#define DECLARE_CONDITIONAL_TYPE_NAME(Name, TypeName, OtherTypeName)    \
struct Name { \
/* 默认:T 没有 TypeName,用 OtherTypeName */ \
template <typename __T, typename = void> \
struct resolve { using type = typename __T::OtherTypeName; }; \
\
/* 特化:T 有 TypeName,优先用它 */ \
template <typename __T> \
struct resolve<__T, \
std::void_t<decltype(sizeof(typename __T::TypeName))>> { \
using type = typename __T::TypeName; \
}; \
}

// 用法:UDF 有没有声明 return_type?
DECLARE_CONDITIONAL_TYPE_NAME(ReturnTypeResolver, return_type, default_return_type);
using ActualReturnType = ReturnTypeResolver::resolve<MyUDF>::type;

三、内存管理

3.1 MemoryPool — 层级内存池

是什么:内存池(Memory Pool)是一种预先向 OS 申请一大块内存,
然后在内部切割分配的技术。与每次 malloc/free 打交道相比,
内存池的分配/释放开销接近零,且可以批量归还,还能精确追踪使用量。

解决什么问题:查询引擎的每个算子(HashAgg、Sort、Join)在执行期间会大量小规模分配内存。
如果每次都走系统 malloc,锁竞争和碎片化会严重影响性能。
此外,多租户场景需要按查询追踪和限制内存用量。
MemoryPool 同时解决了性能和可观测性两个问题。

Velox 例子velox/common/memory/MemoryPool.h

Velox 的 MemoryPool 是树形结构:根节点 → 查询节点 → 算子叶子节点。
每个叶子节点独立分配,父节点汇总统计,支持内存仲裁(当内存紧张时,
仲裁器可以强制某个算子 spill 到磁盘)。

1
2
3
4
5
6
7
8
9
10
// 根池:全局唯一,向 OS 申请内存
auto rootPool = memory::memoryManager()->addRootPool("query_root", maxBytes);

// 算子叶子池:从根池"借"内存,有独立的统计和限制
auto opPool = rootPool->addLeafChild("HashAggregation");

// 从池分配,不走 malloc
void* buf = opPool->allocate(bytes);
opPool->free(buf, bytes);
// 或者 RAII:opPool 析构时自动归还

3.2 raw_vector<T> — 带池分配且 SIMD 友好的向量

是什么raw_vector<T> 是 Velox 自己实现的向量容器,接口类似 std::vector
但有两处关键不同:从 MemoryPool 分配而不是 malloc
在数据区首尾各填充一个 SIMD 宽度的 padding,允许以全 SIMD 宽度安全读写边界元素。
同时它要求 T 必须是 trivially copyable + trivially destructible(用 static_assert 强制),
因此不调用构造/析构,resize 直接 memset

解决什么问题std::vector 的两个问题:一是用 malloc 而非内存池,
二是边界元素无法用 SIMD 指令安全读(可能越界)。
哈希表、Join 等热路径需要既能用池分配,又能在末尾做 SIMD 操作而不越界。

Velox 例子velox/common/memory/RawVector.h:34

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
template <typename T>
class raw_vector {
// 强制:T 必须是 POD-like,否则编译失败
static_assert(
std::is_trivially_destructible_v<T> && std::is_trivially_copyable_v<T>);

public:
// 接受 MemoryPool,从池分配而非 malloc
explicit raw_vector(memory::MemoryPool* pool) : pool_(pool) {}
// ...
};

// HashTable.h 中使用:哈希查找结果存入 pool-backed 向量
struct HashLookup {
explicit HashLookup(memory::MemoryPool& pool)
: rows(raw_vector<vector_size_t>(pool)),
hashes(raw_vector<uint64_t>(pool)),
hits(raw_vector<char*>(pool)) {}

raw_vector<vector_size_t> rows; // 输入行号
raw_vector<uint64_t> hashes; // 哈希值
raw_vector<char*> hits; // 命中的行指针
};

3.3 STL Allocator 适配 — 标准容器接入内存池

是什么:C++ 标准库容器(std::vectorstd::unordered_map 等)
都接受一个 Allocator 模板参数来定制内存分配行为。
通过提供符合 std::allocator_traits 协议的自定义 Allocator,
可以让标准容器从任意内存源(内存池、mmap、GPU 内存)分配。

解决什么问题:聚合、字典等数据结构既需要与标准库容器互操作,
又需要受内存池管理。如果强制用 raw_vector,就无法用 std::map 等复杂容器。
通过 Allocator 模板参数,同一套数据结构既能在测试中用 std::allocator
又能在生产中接入 MemoryPool。

Velox 例子velox/common/hyperloglog/DenseHll.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
template <typename TAllocator>
class DenseHll {
// 从 Allocator 派生出 uint8_t 版本,传给 std::vector
using TStlAllocator = typename TAllocator::template TStlAllocator<uint8_t>;
std::vector<uint8_t, TStlAllocator> registers_;
public:
explicit DenseHll(TAllocator& allocator) : registers_(allocator) {}
};

// 测试:用标准 allocator
DenseHll<StdAllocator> hll(stdAlloc);

// 生产:用 MemoryPool allocator,受池管控
DenseHll<MemoryPoolAllocator> hll(poolAlloc);

四、错误处理

4.1 分层异常体系 + [[noreturn]] 出错路径优化

是什么:C++ 的 [[noreturn]] 属性告诉编译器:这个函数永远不会正常返回(总是抛异常或终止)。
编译器利用这个信息,可以在调用点省略”函数返回后”的代码路径,
使得包含错误检查的小函数仍然可以被内联,而不因为 throw 路径膨胀代码体积。

解决什么问题:错误检查宏如果直接在 macro 里写 throw,会使展开点的函数体积膨胀,
导致原本应该内联的热路径函数无法内联。
把 throw 提取到 [[noreturn]] 的 out-of-line 函数,编译器在调用点只生成一条跳转指令,
热路径体积不变,仍能内联。

Velox 例子velox/common/base/Exceptions.h:50

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 抛异常逻辑完全提取到 out-of-line 函数
template <typename Exception, typename StringType>
[[noreturn]] void veloxCheckFail(const VeloxCheckFailArgs& args, StringType s) {
// 系统错误才打 ERROR 日志,用户错误不打(避免日志刷屏)
if constexpr (!std::is_same_v<Exception, VeloxUserError>) {
LOG(ERROR) << args.file << ":" << args.line << " " << s;
}
throw Exception(args.file, args.line, args.function,
args.expression, s, args.errorSource, args.errorCode,
args.isRetriable);
}

// 宏展开后,检查通过时只有一个 UNLIKELY 分支预测提示 + 跳转
// 不通过时才调用 veloxCheckFail,函数本身不因 throw 变大
#define _VELOX_CHECK_IMPL(expr, exprStr, ...) \
do { \
if (UNLIKELY(!(expr))) { \
veloxCheckFail<VeloxRuntimeError>(..., __VA_ARGS__); \
} \
} while (0)

4.2 两类错误宏 — 系统错误 vs 用户错误

是什么:Velox 把异常分为两类:VeloxRuntimeError(内部 bug,不可重试)
VeloxUserError(SQL 语义错误,不应打 ERROR 日志,可向用户返回友好消息)。
对应两套宏,接口完全相同,只是异常类型不同。

解决什么问题:数据库引擎里,”除以零”是用户的 SQL 写错了,
不应该在服务器日志里打 ERROR;而”哈希表内部状态不一致”是 bug,必须打 ERROR 并报警。
如果混用,要么日志噪音太大,要么真正的 bug 被淹没。

Velox 例子velox/common/base/Exceptions.h:399

1
2
3
4
5
6
7
8
9
10
// 系统错误(内部 bug)— 打 ERROR 日志,抛 VeloxRuntimeError
VELOX_CHECK_LT(idx, size, "Index out of range: {}", idx);
VELOX_CHECK_NOT_NULL(ptr);
VELOX_FAIL("Unexpected encoding type: {}", encoding);
VELOX_UNREACHABLE("This branch should never execute");

// 用户错误(SQL 语义问题)— 不打日志,抛 VeloxUserError
VELOX_USER_CHECK(divisor != 0, "Division by zero");
VELOX_USER_CHECK_EQ(args.size(), 2, "Function requires exactly 2 arguments");
VELOX_USER_FAIL("Column '{}' is ambiguous", name);

4.3 VELOX_DCHECK — 仅在 Debug 模式有效的检查

是什么:条件编译的防御性检查:Debug 构建展开为真正的 CHECK,
Release 构建(NDEBUG 定义时)展开为 VELOX_CHECK(true),即什么都不做。

解决什么问题:热路径上的防御性检查(比如每行数据的边界检查)
在生产环境开销不可接受,但在开发和测试阶段非常有价值。
DCHECK 让这类检查在 Debug 下充分发挥作用,Release 下自动消失,不需要手动注释掉。

Velox 例子

1
2
3
4
5
// 每次访问向量元素时检查下标(每行都会执行,生产环境必须零开销)
VELOX_DCHECK_LT(index, size_, "Vector index out of bounds");

// Release 构建下,上面这行展开为:
VELOX_CHECK(true); // 编译器直接消除

五、SIMD 与向量化

5.1 xsimd — 跨平台 SIMD 抽象

是什么:SIMD(Single Instruction Multiple Data)是一类 CPU 指令,
一条指令同时操作多个数据元素(比如 AVX2 一次处理 256 位,即 32 个 int8)。
直接写 _mm256_* 这样的 intrinsic 代码只能在 x86 运行,换 ARM 就要重写。
xsimd 是一个 header-only 库,提供跨平台的 SIMD 类型和操作,
编译时根据目标架构选择对应的指令集。

解决什么问题:哈希表查找、排序、过滤等热路径的瓶颈往往是”逐元素比较”。
SIMD 能一次比较 16 或 32 个元素,使吞吐量提升数倍。
用 xsimd 抽象层,同一份代码在 x86(SSE2/AVX2)和 ARM(NEON)上都能跑,
不需要维护两套平台代码。

Velox 例子velox/exec/HashTable.h:131

哈希表每个 bucket 有一个 1 字节的 “tag”,存储哈希值的低 8 位。
查找时用 SIMD 一次比较 16 个 tag,只有 tag 匹配的 bucket 才做完整比较:

1
2
3
4
5
6
7
8
9
10
11
// 根据编译目标自动选择 SIMD 类型
#if XSIMD_WITH_SSE2
using TagVector = xsimd::batch<uint8_t, xsimd::sse2>; // x86: 16×uint8
#elif XSIMD_WITH_NEON
using TagVector = xsimd::batch<uint8_t, xsimd::neon>; // ARM: 16×uint8
#endif

// 查找:一次比较 16 个 tag,返回匹配的位掩码
TagVector tags = TagVector::load_unaligned(tagPtr);
TagVector needle(targetTag);
auto matches = (tags == needle).mask(); // 16 位 bitmask,一个 CPU 周期

传统逐个比较需要 16 次迭代;SIMD 只需 1 次,且编译器能自动展开循环。


5.2 SelectivityVector — 位集合批处理

是什么SelectivityVector 是一个位图(bitset),记录当前批次中哪些行被”选中”。
算子之间传递这个位图而不是重排物理数据,过滤掉的行只是位被清零,
数据仍在原位,不需要 memcpy。
位图操作(AND、OR、NOT)可以用 SIMD 一次处理 64 行。

解决什么问题:朴素实现里,每个 filter 会把通过的行拷贝到新数组,
多个 filter 叠加就有多次拷贝。位图方案让多个 filter 的结果只需做位运算合并,
数据零拷贝,且批量迭代时 CPU 分支预测压力小。

Velox 例子velox/vector/SelectivityVector.h:39

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class SelectivityVector {
std::vector<uint64_t> bits_; // 每个 bit 对应一行,64 行 / uint64
vector_size_t begin_{0}; // 有效范围起点(跳过头部空前缀)
vector_size_t end_{0}; // 有效范围终点

public:
// 对所有选中的行执行 func,底层用位扫描,跳过未选中的行
template <typename Callable>
void applyToSelected(Callable func) const {
bits::forEachSetBit(bits_.data(), begin_, end_, func);
}

// 求交集(多个 filter AND):一次 SIMD 操作,不移动数据
void intersect(const SelectivityVector& other) {
bits::andBits(bits_.data(), other.bits_.data(), begin_, end_);
}
};

// Filter 算子:不移动数据,只操作位图
void filterRows(SelectivityVector& rows, const VectorPtr& condition) {
rows.applyToSelected([&](auto row) {
if (!condition->valueAt<bool>(row)) {
rows.setValid(row, false); // 清位,"删除"该行
}
});
rows.updateBounds(); // 更新 begin_/end_ 缩小扫描范围
}

5.3 AVX2 Intrinsics — 解码热路径

是什么:在特别关键的热路径上,xsimd 的抽象层可能引入一点点开销,
此时直接用编译器 intrinsic(如 _mm256_*)写 AVX2 代码。
这是最低层的 SIMD 编程,完全控制生成的指令。

解决什么问题:Parquet/ORC 文件大量使用 bit-pack 压缩:
将多个小整数打包到连续位中,读取时需要解包。
这个解包操作是 IO 的最后一道开销,如果用标量代码逐个解,会浪费大量 CPU。
AVX2 一条指令可以解包 16 个值,是标量代码的 8-16 倍吞吐量。

Velox 例子velox/dwio/common/BitPackDecoder.h

1
2
3
4
// 将 16 个 uint8 零扩展为 16 个 int16,存入 256 位寄存器
// 标量需要 16 次操作;这里 1 条指令完成
__m256i expanded = _mm256_cvtepu8_epi16(input_128);
_mm256_storeu_si256(reinterpret_cast<__m256i*>(dst), expanded);

六、性能工程

6.1 FOLLY_ALWAYS_INLINEFOLLY_LIKELY/UNLIKELY

是什么

  • FOLLY_ALWAYS_INLINE:对应 __attribute__((always_inline)),强制编译器内联函数,
    无论函数体多大,无论编译器的内联启发式认为是否合适。
  • FOLLY_LIKELY/UNLIKELY:对应 __builtin_expect,向编译器提示分支概率,
    让编译器把”大概率”分支的代码排在前面,减少 CPU 分支预测错误(misprediction)。

解决什么问题:编译器的内联决策基于代码大小启发式,
有时会拒绝内联本应内联的关键函数(比如一个简单的 compare 函数因为调用了另一个函数而被拒绝)。
分支预测错误每次有 10-20 个时钟周期的惩罚;在”检查是否为 null”这种每行都执行的代码里,
正确的分支提示能带来明显提升。

Velox 例子velox/exec/PrefixSort.h:130velox/exec/TableWriter.h:68

1
2
3
4
5
6
7
8
9
10
11
12
// 排序比较函数:必须内联,否则每行排序都有函数调用开销
FOLLY_ALWAYS_INLINE static void sort(
char* rows, int32_t numRows, const PrefixSortLayout& layout) {
// ... 内联后编译器可以进一步优化整个排序循环
}

// 写数据时,dataSink_ 不为 null 是绝大多数情况
if (FOLLY_LIKELY(dataSink_ != nullptr)) {
dataSink_->write(data); // 热路径,CPU 预测这里
} else {
handleNullSink(); // 冷路径,几乎不发生
}

6.2 移动语义与完美转发

是什么:C++11 的移动语义(std::move)和完美转发(std::forward
允许在不拷贝数据的前提下转移对象所有权。
移动操作通常只需要交换几个指针,是 O(1) 操作,而拷贝是 O(n)。
完美转发(配合 && 引用折叠)让包装函数可以把参数”原样”传递给内层函数,
不引入额外拷贝。

解决什么问题:查询引擎里大量传递 std::vector<RowVectorPtr>
std::shared_ptr<PlanNode> 等”重”对象。
如果按值传递且不移动,每次调用都要拷贝整个 vector(可能含数百 MB 数据)。
移动语义让所有权转移从 O(n) 降到 O(1)。

Velox 例子velox/core/PlanNode.hvelox/core/Expressions.h:211

1
2
3
4
5
6
7
8
9
10
11
12
13
// Builder 模式:接受右值,所有权转移,不拷贝
Builder& values(std::vector<RowVectorPtr> vals) {
values_ = std::move(vals); // vector 内容转移,O(1)
return *this;
}

// 可变参数完美转发:inputs 可以是左值或右值,原样转发给 vector 构造
template <typename... TypedExprs>
CallTypedExpr(TypePtr type, std::string name, TypedExprs... inputs)
: CallTypedExpr(
std::move(type),
std::vector<TypedExprPtr>{std::forward<TypedExprs>(inputs)...},
std::move(name)) {}

6.3 constexpr 与编译期常量

是什么constexpr 变量和函数在编译期求值,结果直接嵌入可执行文件。
constexpr 常量放在 .rodata 段,CPU 第一次访问后会缓存在指令缓存中,
后续访问几乎零延迟。

解决什么问题:热路径上如果有”计算一个只依赖于类型或配置的常量”的操作,
每次调用都重算是浪费。编译期常量消除这类开销,同时让代码保持可读。

Velox 例子velox/exec/HashTable.h:140

1
2
3
4
5
6
7
8
9
10
11
12
// 哈希表参数:编译期固定,编译器可以用于优化(比如 resize 的乘法可以用 shift 替代)
static constexpr double kHashTableLoadFactor = 0.7;
static constexpr uint64_t kArrayHashMaxSize = 2L << 20; // 2M entries

// 运行时统计 key 名:std::string_view 指向 .rodata,不堆分配
static constexpr std::string_view kCapacity {"hashtable.capacity"};
static constexpr std::string_view kNumRehashes {"hashtable.numRehashes"};

// BitUtil.h:查表代替运算,编译器把表放进 .rodata
static constexpr uint8_t kZeroBitmasks[] = {
0xFF, 0xFE, 0xFC, 0xF8, 0xF0, 0xE0, 0xC0, 0x80
};

七、Folly 库深度集成

7.1 folly::Synchronized<T> — 数据与锁绑定

是什么folly::Synchronized<T> 将一个值和一把锁打包在一起,
强制所有访问都必须先获取锁。它提供 rlock()(读锁)和 wlock()(写锁),
返回 RAII 守卫,守卫销毁时自动释放锁。

解决什么问题:传统的 mutex + data 模式,数据和锁是独立的成员,
调用者可能”忘记加锁”就直接访问数据,且代码审查时很难发现。
Synchronized 让访问数据的唯一方式就是先拿锁,从语言层面杜绝忘加锁。

Velox 例子velox/exec/Operator.h:358velox/exec/OutputBufferManager.h:138

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Operator 统计信息:多线程读写,Synchronized 保证安全
class Operator {
folly::Synchronized<OperatorStats> stats_;

public:
// 外部只能通过 rlock/wlock 访问,不能绕过
folly::Synchronized<OperatorStats>& stats() { return stats_; }

void addRuntimeStat(const std::string& name, int64_t value) {
// wlock 是 RAII,函数返回时自动解锁
stats_.wlock()->runtimeStats[name] += value;
}
};

// OutputBuffer:folly::Synchronized 保护内部状态
folly::Synchronized<
folly::F14FastMap<int32_t, std::shared_ptr<OutputBuffer>>> buffers_;

7.2 folly::F14FastMap/Set — SIMD 加速的哈希容器

是什么:F14 是 Facebook 开发的开放寻址哈希表,每个 “chunk” 存 14 个条目,
chunk 的 tag 字节用 SIMD 指令一次比较 14 个,找到匹配的 slot。
相比 std::unordered_map(链地址法,大量指针追逐),
F14 的 cache 局部性更好,find 操作快约 2-3x,内存占用也更低。

解决什么问题std::unordered_map 每个桶是一个链表,
查找时要追踪指针,对 CPU cache 极不友好。
执行引擎里哈希表是高频操作(Join、GroupBy、分区路由),
哈希容器的性能直接影响整体吞吐。

Velox 例子velox/exec/Spiller.h:163velox/exec/Task.h:1458

1
2
3
4
5
6
7
8
// Spiller 用 F14FastMap 追踪每个分区的 spill 状态
folly::F14FastMap<SpillPartitionId, SpillRun> spillRuns_;

// Task 用 F14FastSet 追踪已分配的 split,避免重复分配
folly::F14FastSet<std::shared_ptr<connector::ConnectorSplit>> splits_;

// MergeJoin 的过滤列索引映射
folly::F14FastMap<column_index_t, column_index_t> filterInputToOutputChannel_;

7.3 folly::Executor — 解耦执行引擎与线程调度

是什么folly::Executor 是一个抽象接口,表示”可以提交任务的执行环境”,
子类可以是线程池、单线程队列、inline executor 等。
代码面向 Executor* 编程,不直接创建线程。

解决什么问题:执行引擎如果自己管线程,会和上层框架(Presto、Spark)的线程池冲突,
造成线程数膨胀和调度混乱。
通过接受 folly::Executor*,Velox 把线程调度的控制权完全交给调用方,
自己只负责”提交任务”。

Velox 例子velox/exec/HashTable.h:368

1
2
3
4
5
6
7
8
9
10
11
// 哈希表并行构建:executor 由调用方提供(可能是 Presto 的线程池)
virtual void parallelJoinBuild(
std::vector<std::unique_ptr<BaseHashTable>> partitions,
folly::Executor* executor = nullptr) = 0;

// Spiller 也接受 executor 做异步 IO
class Spiller {
folly::Executor* const executor_;
public:
explicit Spiller(folly::Executor* executor) : executor_(executor) {}
};

八、设计模式

8.1 Builder 模式 + std::optional 延迟校验

是什么:Builder 模式将一个复杂对象的构建过程与其表示分离,
允许分步骤设置参数,最后调用 build() 得到完整对象。
std::optional 标记每个参数是否已设置,build() 统一校验必填项,
而不是在每个 setter 里提前校验(彼时上下文不完整)。

解决什么问题:PlanNode 有十几个参数,部分必填、部分可选,且参数之间有约束。
用构造函数直接传参,参数多了难以阅读且容易搞混位置。
Builder 让每个参数有名字,且 build() 是产生合法对象的唯一入口,
不可能产生”半初始化”的 PlanNode。

Velox 例子velox/core/PlanNode.h:348

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class ValuesNode::Builder {
std::optional<PlanNodeId> id_;
std::optional<std::vector<RowVectorPtr>> values_;
std::optional<bool> parallelizable_;

public:
Builder& id(PlanNodeId id) {
id_ = std::move(id); return *this;
}
Builder& values(std::vector<RowVectorPtr> v) {
values_ = std::move(v); return *this;
}

std::shared_ptr<ValuesNode> build() const {
VELOX_USER_CHECK(id_.has_value(), "ValuesNode id is not set");
VELOX_USER_CHECK(values_.has_value(), "ValuesNode values is not set");
return std::make_shared<ValuesNode>(*id_, *values_, parallelizable_.value_or(false));
}
};

// 调用方代码清晰,参数有名字,顺序不重要
auto node = ValuesNode::Builder{}
.id("values-1")
.values(std::move(data))
.parallelizable(true)
.build();

8.2 Visitor 模式 — Plan Tree 遍历

是什么:Visitor 模式将”对数据结构的操作”与”数据结构本身”解耦。
数据结构(PlanNode、Expr)只提供 accept(visitor) 接口,
具体操作(打印、代价估计、验证、代码生成)由不同的 Visitor 类实现。
添加新操作不需要修改节点类,符合开放/封闭原则。

解决什么问题:查询计划有十几种节点类型,对计划的操作(优化、打印、序列化)有几十种。
如果把所有操作都放在节点类里,每个节点类会变得臃肿,且每次新增操作都要改所有节点类。
Visitor 让操作独立,互不干扰。

Velox 例子velox/core/PlanNode.h:184

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 节点只声明 accept,不知道 visitor 会做什么
class PlanNode {
public:
virtual void accept(
const PlanNodeVisitor& visitor,
PlanNodeVisitorContext& context) const = 0;
};

// 不同操作各自实现 Visitor
class PlanPrinter : public PlanNodeVisitor {
void visit(const FilterNode& node, PlanNodeVisitorContext&) const override {
out_ << "Filter[" << node.filter()->toString() << "]";
}
void visit(const HashJoinNode& node, PlanNodeVisitorContext&) const override {
out_ << "HashJoin[" << joinTypeString(node.joinType()) << "]";
}
};

8.3 Strategy 模式 — Connector 插件体系

是什么:Strategy 模式将一类算法(数据读取、写入)抽象为接口,
不同实现(Hive、Iceberg、自定义)作为可替换的”策略”插入。
调用方面向接口编程,不感知底层实现。

解决什么问题:Velox 需要同时支持 S3、GCS、HDFS、本地文件、Iceberg 等数据源,
每种数据源的 IO 细节完全不同。
通过 Connector 接口,执行引擎与存储完全解耦,新增存储只需实现接口,
不需要修改执行引擎代码。

Velox 例子velox/connectors/Connector.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 抽象接口:执行引擎只认识这个
class Connector {
public:
virtual std::shared_ptr<DataSource> createDataSource(
const RowTypePtr& outputType,
const ConnectorTableHandle& tableHandle,
memory::MemoryPool* pool) = 0;

virtual std::shared_ptr<DataSink> createDataSink(
RowTypePtr inputType,
const ConnectorInsertTableHandle& handle) = 0;
};

// 具体实现:HiveConnector 读 S3/HDFS
// IcebergConnector 读 Iceberg 表
// FuzzerConnector 生成随机数据(用于测试)
// 三者对执行引擎完全透明

九、向量化执行引擎

9.1 多级编码 Vector 体系

是什么:Velox 的列向量(BaseVector)支持多种物理编码:
Flat(连续存储)、Dictionary(索引 + 去重值)、Constant(整列同值)、
Bias(整数偏移压缩)、LazyVector(延迟 IO)。
这些编码可以嵌套(Dictionary of Dictionary of Constant),
而执行引擎不需要知道底层编码,通过统一的 valueAt(idx) 接口访问。

解决什么问题:实际数据中大量重复值(城市、类别、枚举)用 Dictionary 编码可以节省内存;
常量折叠后整列相同的值用 Constant 编码,算子可以直接跳过;
IO 时用 LazyVector 延迟加载,只有真正被引用的列才读磁盘。
统一接口让算子代码不因编码类型而分叉。

Velox 例子velox/vector/DictionaryVector.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Dictionary 编码:indices_[i] 指向 dictionaryValues_ 中的唯一值
template <typename T>
class DictionaryVector : public SimpleVector<T> {
BufferPtr indices_; // 每行的字典下标
VectorPtr dictionaryValues_; // 去重后的实际值(可以更小)

public:
T valueAt(vector_size_t idx) const override {
auto dictIdx = getDictionaryIndex(idx);
return dictionaryValues_->valueAt<T>(dictIdx);
}

// if constexpr 区分复杂类型和标量类型:编译期分支,无运行时开销
bool containsNullAt(vector_size_t idx) const override {
if constexpr (std::is_same_v<T, ComplexType>) {
if (isNullAt(idx)) return true;
return dictionaryValues_->containsNullAt(getDictionaryIndex(idx));
} else {
return isNullAt(idx);
}
}
};

9.2 DecodedVector — 透明展平多级编码

是什么DecodedVector 是一个”解码视图”,
将任意层数的嵌套编码(Dict of Dict of Constant of Flat)展平为一个连续的逻辑视图。
它在构造时遍历编码链,合并索引和 null bitmap,之后访问是 O(1) 的。
内存从系统分配器分配(而非内存池),因为它可以在多次调用间复用。

解决什么问题:如果每个算子都要自己处理嵌套编码,代码会极其复杂,
且很容易遗漏某种编码组合。
DecodedVector 提供统一入口,算子只需要一行 DecodedVector decoded(*vec, rows)
之后就像访问 FlatVector 一样访问数据。

Velox 例子velox/vector/DecodedVector.h

1
2
3
4
5
6
7
8
9
10
// Filter 算子:不管输入是什么编码,DecodedVector 统一处理
void applyFilter(VectorPtr input, SelectivityVector& rows) {
DecodedVector decoded(*input, rows); // 展平,O(size) 一次性开销
rows.applyToSelected([&](auto row) {
// decoded.valueAt 是 O(1),底层自动解引用索引链
if (!decoded.valueAt<bool>(row)) {
rows.setValid(row, false);
}
});
}

9.3 LazyVector — 延迟物化与 IO

是什么LazyVector 是一种特殊的 Vector,内部存储一个”加载器”(loader),
而不是实际数据。数据只在第一次被访问时(调用 loadedVector())才真正执行 IO。

解决什么问题:TableScan 读取一个包含 100 列的宽表,SQL 只用了 5 列。
如果 scan 阶段就把所有 100 列都读出来,磁盘 IO 和内存都是严重浪费。
LazyVector 让 scan 先生产占位符,Filter/Project 先用已物化的列做谓词下推,
只有通过过滤的行的所需列才触发 IO,实现精确的行列裁剪。

Velox 例子velox/vector/LazyVector.h

1
2
3
4
5
6
7
8
9
// TableScan 生产 LazyVector,内含 IO 加载器
auto lazyColumn = std::make_shared<LazyVector>(
pool_, type_, numRows,
std::make_unique<ColumnReader>(rowGroup_, columnIdx_));

// 算子链中:Filter 先在已物化的谓词列上工作
// 只有 Project 真正需要某列时才调用:
lazyColumn->load(selectedRows, false); // 只加载 selectedRows 中的行
auto loaded = lazyColumn->loadedVector();

十、Arrow 零拷贝集成

10.1 Arrow C Data Interface

是什么:Arrow C Data Interface 是一个二进制级别的协议,
允许两个进程或两个库在同一进程内共享 Arrow 格式的列数据,
而不需要序列化/反序列化,也不需要拷贝数据。
协议通过两个 C 结构体(ArrowSchemaArrowArray)定义数据布局,
以及一个 release 回调管理内存所有权。

解决什么问题:不同的数据引擎(Velox、DuckDB、DataFusion、pandas)
内部表示不同,互操作时必须序列化+拷贝,对大数据集代价极高。
Arrow C Data Interface 让它们共享同一块内存,任何一方都可以安全持有引用,
最后持有引用的一方通过 release 回调释放内存。

Velox 例子velox/vector/arrow/Bridge.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Velox → Arrow:转移所有权,不拷贝任何数据
void exportToArrow(
VectorPtr vector,
ArrowArray* out,
memory::MemoryPool* pool);

// Arrow → Velox:包装 Arrow buffer,同样不拷贝
VectorPtr importFromArrow(
const ArrowSchema& schema,
const ArrowArray& array,
memory::MemoryPool* pool);

// 原理:Velox buffer 的引用计数通过 ArrowArray::release 回调管理
// Arrow 侧 release 时,Velox 侧的 shared_ptr 引用计数减一
// 整个生命周期由 C++ RAII 和 release 回调共同管理,无 GC 介入

十一、并发设计

11.1 原子变量 — 无锁计数器与状态机

是什么std::atomic<T> 提供对 T 的原子读/写/CAS(Compare-And-Swap)操作,
不需要锁。对于简单的计数器,fetch_add 是一条 CPU 指令(LOCK XADD),
mutex + ++counter 轻量很多。

解决什么问题:多线程并发统计(如 IO 耗时、行数、字节数)如果用锁,
锁竞争本身会成为瓶颈。原子变量让每个线程各自更新,CPU 硬件保证原子性。
状态机的状态转移用 CAS 保证”只有一个线程成功转移”,无需锁。

Velox 例子velox/connectors/hive/FileDataSource.h

1
2
3
4
5
6
7
8
9
10
11
// IO 线程并发累加耗时,memory_order_relaxed 足够(只要最终一致)
std::atomic_uint64_t totalRemainingFilterTime_{0};
totalRemainingFilterTime_.fetch_add(elapsed, std::memory_order_relaxed);

// Exchange Source 状态机:CAS 保证只有一个线程成功关闭
std::atomic<ReceiverState> state_;
ReceiverState expected = ReceiverState::kRunning;
if (state_.compare_exchange_strong(expected, ReceiverState::kDone)) {
// 只有 CAS 成功的线程执行清理,其他线程看到状态已变,直接返回
closeChannel();
}

11.2 Task / Driver / Operator 无共享并发模型

是什么:Velox 采用 shared-nothing 并发模型:

  • Task:一个查询分片,拥有若干 Pipeline
  • Driver:对应一个执行线程,拥有一条 Operator 链,单线程顺序执行
  • 同一 Task 内的多个 Driver 之间通过有界内存队列交换数据,不共享可变状态

这与 Golang 的 goroutine + channel 模型思路相同,把并发隔离为消息传递。

解决什么问题:多线程共享可变状态需要大量锁,锁的粒度设计复杂,
死锁和竞争条件难以调试。
shared-nothing 模型让每个 Driver 是单线程的,
大多数代码不需要考虑并发,只有 Driver 边界(队列)需要同步原语。

Velox 例子:执行一个有 4 个 Driver 的 HashAgg:

1
2
3
4
Driver 0: Scan → Filter → Project → PartialAgg ─┐
Driver 1: Scan → Filter → Project → PartialAgg ─┤→ Exchange Queue → Driver 4: FinalAgg
Driver 2: Scan → Filter → Project → PartialAgg ─┤
Driver 3: Scan → Filter → Project → PartialAgg ─┘

Driver 0-3 完全独立,各自用各自的内存池、各自的向量。
唯一的同步点是 Exchange Queue(folly::MPMCQueue 或类似结构)。
FinalAgg 的 Driver 是单消费者,不需要在 Operator 内部加锁。


十二、CRTP — 静态多态与可复用 Builder

是什么:CRTP(Curiously Recurring Template Pattern,奇异递归模板模式)
是让派生类把自身类型作为模板参数传给基类:class Derived : public Base<Derived>
基类通过 static_cast<Derived&>(*this) 向下转型,从而在不使用虚函数的情况下
实现”基类调用派生类方法”的效果,即静态多态

解决什么问题:PlanNode 的 Builder 模式面临代码复用困境:
HashJoinNode::BuilderProjectNode::BuilderMergeJoinNode::Builder
都有相同的 id()source() 等 setter,直接用继承会导致 setter 返回基类引用,
链式调用断掉(builder.id("x").source(src).leftKeys(...)id() 返回 Base&
找不到 leftKeys())。
CRTP 让基类 setter 返回 DerivedBuilder&,派生 Builder 无需重写这些 setter。

Velox 例子velox/core/PlanNode.h:770

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 基类 Builder:DerivedPlanNode 是最终节点类型,DerivedBuilder 是子类 Builder
template <typename DerivedPlanNode, typename DerivedBuilder>
class AbstractProjectNode::Builder {
public:
// setter 返回 DerivedBuilder& 而非 Builder&,链式调用不断链
DerivedBuilder& id(PlanNodeId id) {
id_ = std::move(id);
return static_cast<DerivedBuilder&>(*this); // CRTP 向下转型
}
DerivedBuilder& names(std::vector<std::string> names) {
names_ = std::move(names);
return static_cast<DerivedBuilder&>(*this);
}

protected:
std::optional<PlanNodeId> id_;
std::optional<std::vector<std::string>> names_;
// ...
};

// 派生 Builder:只需实现 build(),通用 setter 全部继承自基类
class ProjectNode::Builder
: public AbstractProjectNode::Builder<ProjectNode, Builder> {
public:
std::shared_ptr<ProjectNode> build() const {
VELOX_USER_CHECK(id_.has_value(), "ProjectNode id is not set");
return std::make_shared<ProjectNode>(*id_, *names_, *projections_, *source_);
}
};

// 调用方:链式调用畅通无阻,id()/names() 来自基类,build() 来自派生类
auto node = ProjectNode::Builder{}
.id("proj-1") // 返回 ProjectNode::Builder&
.names({"a", "b"}) // 返回 ProjectNode::Builder&
.projections({...})
.source(child)
.build();

enable_shared_from_this 是另一处经典 CRTP:

1
2
3
4
5
6
// velox/core/QueryCtx.h
class QueryCtx : public std::enable_shared_from_this<QueryCtx> {
public:
// 在成员函数内安全获取指向自身的 shared_ptr,不创建新的控制块
auto self = shared_from_this();
};

十三、Policy-based Design — 编译期行为注入

是什么:策略模式的编译期版本:将可变行为(算法参数、开关选项)
定义为一个只含 static constexpr 成员的空结构体(Policy Struct),
作为模板参数注入到实现类中。
不同的 Policy 产生不同的特化版本,编译器在实例化时直接把 Policy 的常量内联进代码,
不需要运行时 if-else,也没有虚函数调用。

解决什么问题:Presto 和 Spark 的类型转换规则不同(截断行为、Unicode 处理、遗留模式),
如果用运行时 flag 区分,每次类型转换都要多一次分支。
用 Policy 模板参数,Presto 路径和 Spark 路径在编译期就分开,
各自生成最优代码,无运行时判断开销。

Velox 例子velox/type/Conversions.h:36

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 三种 Policy:只有 constexpr 常量,无数据成员,大小为零
struct PrestoCastPolicy {
static constexpr bool truncate = false; // 溢出报错
static constexpr bool legacyCast = false;
static constexpr bool throwOnUnicode = true; // 遇到 unicode 报错
};

struct SparkCastPolicy {
static constexpr bool truncate = true; // 溢出截断
static constexpr bool legacyCast = false;
static constexpr bool throwOnUnicode = false;
};

struct LegacyCastPolicy {
static constexpr bool truncate = false;
static constexpr bool legacyCast = true; // 兼容旧行为
static constexpr bool throwOnUnicode = false;
};

// 实现类:TPolicy 控制所有行为分支,编译期常量消除死代码
template <TypeKind KIND, typename = void, typename TPolicy = PrestoCastPolicy>
struct Converter {
template <typename TFrom>
static Expected<TTo> tryCast(TFrom value) {
if constexpr (TPolicy::truncate) {
return truncateAndCast(value); // Spark 路径
} else {
return strictCast(value); // Presto 路径
}
// 编译器将未选中的分支完全消除
}
};

// 使用:每种引擎实例化不同特化,无运行时开销
Converter<TypeKind::INTEGER, void, SparkCastPolicy>::tryCast(3.9f); // → 3
Converter<TypeKind::INTEGER, void, PrestoCastPolicy>::tryCast(3.9f); // → error

十四、StringView — 小字符串优化 (SSO) 与前缀缓存

是什么:小字符串优化(SSO,Small String Optimization)是一种将短字符串
直接存储在对象本体内(避免堆分配)的技术,std::string 通常也内置了 SSO。
Velox 的 StringView 在此基础上增加了”前缀缓存”:
对于长字符串,除了存储堆指针,还缓存前 4 个字节。
比较时先比长度,再比前缀,前缀不等则直接 short-circuit,
只有前缀相等才做完整比较,大幅减少缓存 miss。

解决什么问题:列式引擎里字符串比较是高频操作(Join、Sort、Filter)。
普通 std::string 每次比较都要追踪堆指针,对大字符串集合 cache miss 严重。
StringView 的 16 字节固定大小使其可以存进向量,前缀缓存让绝大多数不等比较
只需看前 8 字节(4 字节大小 + 4 字节前缀),不需要访问堆内存。

Velox 例子velox/type/StringView.h:72

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
struct StringView {
// 固定 16 字节,可以放进 FlatVector<StringView>
static_assert(sizeof(StringView) == 16);

static constexpr size_t kPrefixSize = 4; // 前缀缓存 4 字节
static constexpr size_t kInlineSize = 12; // ≤12 字节:完全内联,无堆分配

// 内存布局:
// [4B size_] [4B prefix_] [8B value_.data 或内联数据]
int32_t size_;
char prefix_[4];
union {
const char* data; // 长字符串:堆指针
char inlined[8]; // 短字符串:继续存数据
} value_;

bool operator==(const StringView& other) const {
// 1. 比长度(1 次整数比较)
if (size_ != other.size_) return false;
// 2. 比前 8 字节(含 prefix)——覆盖 ≤8 字节的短字符串和所有长字符串前缀
if (*reinterpret_cast<const int64_t*>(prefix_) !=
*reinterpret_cast<const int64_t*>(other.prefix_)) return false;
// 3. 只有前缀相等时才 memcmp 完整内容
return memcmp(data(), other.data(), size_) == 0;
}
};

// 防止悬空引用:禁止从临时 std::string 构造(右值重载 = delete)
explicit StringView(std::string&& value) = delete; // ← 关键!
explicit StringView(folly::fbstring&& value) = delete; // 右值生命期短于 view

= delete 禁用右值重载是一种主动防御:
编译器会在你写 StringView sv = std::string("tmp") 时报错,
而不是让你创建一个指向已析构内存的悬空指针。


十五、folly::Expected<T, E> — 无异常错误传播

是什么folly::Expected<T, E>(类似 Rust 的 Result<T, E>、C++23 的 std::expected
是一个类型安全的”要么值要么错误”容器。
函数返回 Expected<T, E> 而不是抛异常,调用方通过 .value().error()
.hasValue()folly::makeUnexpected() 处理结果。
错误在调用链上是显式的、可组合的,不需要 try-catch。

解决什么问题:在底层 IO 和压缩路径上,异常代价高(栈展开、堆分配)
且难以与 C 库或 GPU 代码集成。
Expected 让错误以返回值形式传播,零开销(success 路径就是普通函数返回),
且调用者被迫处理错误(不像异常可以悄悄向上传播)。

Velox 例子velox/common/compression/Compression.h:78velox/type/Conversions.h:68

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// Codec 接口:创建可能失败,但不抛异常
class Codec {
public:
// 返回 Expected:成功是 unique_ptr<Codec>,失败是 Status(含错误信息)
static Expected<std::unique_ptr<Codec>> create(
CompressionKind kind,
const CodecOptions& options = {});

// 解压也可能失败:返回实际解压字节数或错误
virtual Expected<uint64_t> decompress(
const uint8_t* input, uint64_t inputLen,
uint8_t* output, uint64_t outputLen) = 0;
};

// 调用方:显式处理错误,无 try-catch 开销
auto codec = Codec::create(CompressionKind_ZSTD);
if (!codec.hasValue()) {
LOG(ERROR) << "Failed to create codec: " << codec.error().message();
return;
}
auto result = codec.value()->decompress(src, srcLen, dst, dstLen);

// 类型转换:try 语义,失败返回 Unexpected 而非抛异常
template <typename TPolicy = PrestoCastPolicy>
static Expected<int64_t> tryCast(double value) {
if (std::isnan(value)) {
return folly::makeUnexpected(Status::UserError("NaN cannot be cast to integer"));
}
return static_cast<int64_t>(value);
}

十六、[[nodiscard]] — 强制检查返回值

是什么:C++17 属性 [[nodiscard]],可以标注在函数或整个类上。
标注后,如果调用方忽略返回值(不用变量接收),编译器发出警告。
标注在类上时,任何返回该类型的函数都自动带有 nodiscard 语义。

解决什么问题Status 类表示操作结果,如果调用方忘记检查
(常见 bug:file.close(); // 返回的 Status 被丢弃),
错误会悄悄被忽略。[[nodiscard]] 把这类 bug 提升为编译期警告,
不需要运行时才能发现。

Velox 例子velox/common/base/Status.h:194

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 整个类标注 [[nodiscard]]:所有返回 Status 的函数都被监控
class [[nodiscard]] Status {
public:
constexpr Status() noexcept : state_(nullptr) {} // 成功状态

static Status OK() { return Status(); }

template <typename... Args>
static Status UserError(Args&&... args) { ... } // 失败状态

bool ok() const { return state_ == nullptr; }
};

// 调用方如果写:
writeData(buffer); // ← 编译警告:[[nodiscard]] Status 被忽略
VELOX_CHECK_OK(writeData(buffer)); // ← 正确:检查并在失败时 throw

同理,ExceptionContextSetter 也标注了 [[nodiscard]]

1
2
3
4
5
6
7
class [[nodiscard]] ExceptionContextSetter {
// 必须用变量接收,否则 RAII 析构立即执行,上下文设置无效
};
// 错误用法(编译警告):
ExceptionContextSetter(ctx); // 临时对象立即析构
// 正确用法:
auto setter = ExceptionContextSetter(ctx); // 持有到作用域结束

十七、folly::Range — 非拥有数组视图

是什么folly::Range<T*> 是一个非拥有的连续内存视图,
存储 (begin, end) 指针对,提供类似 std::span(C++20)的接口。
T 可以是 char*const int32_t* 等任意指针类型。
它不管理内存生命期,只是一个”窗口”。

解决什么问题:传递数组时常见两种做法:

  • vector&:强制调用方用 vector,且可能触发拷贝
  • T* + size:不安全,容易越界

folly::Range 是两者的替代:零拷贝、边界安全、与 std::vector/std::array/原始数组都兼容。
Velox 用它替代裸指针对,让接口更清晰、更难出错。

Velox 例子velox/exec/RowContainer.h:41

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// RowContainer 的接口:操作一批行指针,不拥有数据
class RowContainer {
public:
// folly::Range<char**>:一个指针的数组视图,char** 指向行数据指针
void eraseRows(folly::Range<char**> rows);

// 提取一批行到向量:rows 是行指针范围
void extractSerializedRows(
folly::Range<char**> rows,
const VectorPtr& result);

// 追加分区信息:Range 包裹 uint8 数组
void appendPartitions(folly::Range<const uint8_t*> partitions);
};

// Accumulator 的析构回调:接受 Range 而非 vector,调用方可传任意连续内存
Accumulator(
std::function<void(folly::Range<char**> groups, VectorPtr& result)> spillExtract,
std::function<void(folly::Range<char**> groups)> destroy);

// BitPackDecoder:RowSet 就是行号的 Range
using RowSet = folly::Range<const vector_size_t*>;
void unpackBits(RowSet rows, const uint64_t* bits, uint64_t* output);

十八、结构化绑定 (Structured Bindings) — 多返回值

是什么:C++17 的结构化绑定(auto [a, b] = expr)允许把
std::pairstd::tuple、结构体、std::map::insert 返回值等
一次性解构为多个具名变量,不再需要 .first/.secondstd::get<0>

解决什么问题:返回多个值的函数在 C++17 前通常用 std::pair 或 out 参数,
访问时要写 .first/.second,代码难以阅读(看不出语义)。
结构化绑定让返回多值的函数变得像 Python tuple unpacking 一样直观。

Velox 例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// FileSplitReader.cpp:解构 decimal 类型的精度和小数位
auto [precision, scale] = getDecimalPrecisionScale(*type);
auto decimalType = DECIMAL(precision, scale);

// HiveDataSink.cpp:解构文件名对
auto [targetFileName, writeFileName] = getWriterFileNames(bucketId);
initWriter(targetFileName, writeFileName);

// 解构 map::emplace 返回的 (iterator, bool) 对
auto [it, inserted] = stats.emplace(key, metric);
if (!inserted) {
it->second += metric; // key 已存在,累加
}

// 解构压缩结果
auto [decompressedLength, exact] = getDecompressedLength(src, srcLength);

十九、noexcept — 移动语义与性能保证

是什么noexcept 声明函数不会抛异常。它有两个作用:

  1. 性能std::vector resize 时,只有移动构造函数标记了 noexcept
    才会用移动而非拷贝(否则为保证异常安全必须拷贝)。
  2. 文档:告诉调用方和编译器这个路径不会有异常,
    编译器可以省略异常处理 prologue/epilogue,生成更小的代码。

解决什么问题Status 需要放进 std::vector,如果移动构造不是 noexcept
vector 扩容时会拷贝所有元素(O(n) 拷贝 vs O(1) 移动)。
此外,默认构造函数(创建 OK 状态)是极高频操作,constexpr + noexcept 让它在编译期完成。

Velox 例子velox/common/base/Status.h:197

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class [[nodiscard]] Status {
public:
// constexpr + noexcept:OK 状态在编译期构造,运行时零开销
constexpr Status() noexcept : state_(nullptr) {}

// noexcept 析构:vector 扩容时安全
~Status() noexcept {
if (FOLLY_UNLIKELY(state_ != nullptr)) {
deleteState();
}
}

// noexcept 移动:允许 vector<Status> 在 resize 时用移动而非拷贝
Status(Status&& s) noexcept : state_(s.state_) {
s.state_ = nullptr; // 转移所有权,不抛异常
}
Status& operator=(Status&& s) noexcept {
std::swap(state_, s.state_);
return *this;
}

// noexcept 比较:可以在 noexcept 上下文中使用
bool operator==(const Status& other) const noexcept;
};

二十、类型双关 (reinterpret_cast) — 位级操作

是什么:类型双关(Type Punning)是将一块内存重新解释为不同类型,
在 C++ 中通过 reinterpret_cast 实现(C++20 起推荐用 std::bit_cast,更安全)。
用途:把 float 的位模式当 uint32_t 处理,或从字节流读取结构体,
而不触发真正的类型转换(不改变位的值,只改变解释方式)。

解决什么问题:哈希函数需要把 float/double 的位模式当整数处理
(否则 NaN 有无数种位模式,+0-0 位模式不同但语义相等)。
序列化需要把结构体直接按字节写出。
这些操作用 static_cast 会做数值转换,用 reinterpret_cast 则直接重解释位模式。

Velox 例子velox/connectors/hive/HivePartitionFunction.cpp:28

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// double 的哈希:把 64 位浮点的位模式当 uint64_t 处理
// 不能用 static_cast<uint64_t>(value),那会做数值转换
uint32_t hashDouble(double value) {
return ((*reinterpret_cast<uint64_t*>(&value)) >> 32)
^ static_cast<uint32_t>(*reinterpret_cast<uint64_t*>(&value));
}

// float 的哈希:位模式复用为 int32
uint32_t hashFloat(float value) {
return static_cast<uint32_t>(*reinterpret_cast<const int32_t*>(&value));
}

// BitUtil.h:从任意位偏移读取 T 宽度的值(位流解码核心操作)
template <typename T>
T loadBits(const void* bits, uint64_t bitOffset) {
auto address = reinterpret_cast<uint64_t>(bits) + bitOffset / 8;
return *reinterpret_cast<const T*>(address);
}

二十一、constexpr 查找表 — 以空间换时间

是什么:把需要在运行时计算的映射关系预计算成 constexpr 数组,
存放在 .rodata 段(只读数据段)。
程序启动时数组已就绪,访问只需一次数组下标,
且因为数组固定在 .rodata,CPU L1 cache 能长期持有热点部分。

解决什么问题:Parquet/ORC 的 bit-pack 解码需要对每种位宽(1-32 bit)
生成 PDEP 指令的掩码。如果每次解码都现算,需要复杂的位移运算;
预计算成 256 字节表,查找只需 kPdepMask8[width],一条内存读指令。

Velox 例子velox/dwio/common/BitPackDecoder.h:30

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// PDEP 掩码表:对每种位宽(0-8),预算好用于 _pdep_u64 的掩码
// 编译期生成,放进 .rodata,程序运行时不再计算
static constexpr const uint64_t kPdepMask8[] = {
0x0000000000000000, // 0 bit/value
0x0101010101010101, // 1 bit/value
0x0303030303030303, // 2 bit/value
// ...
0xffffffffffffffff, // 8 bit/value
};

// 使用:一条数组访问代替复杂的位运算
uint64_t mask = kPdepMask8[bitsPerValue];
uint64_t packed = _pdep_u64(raw, mask); // BMI2 指令展开位

// 同理,16/32 位也有各自的掩码表
static constexpr const uint64_t kPdepMask16[17] = { ... };
static constexpr const uint64_t kPdepMask32[33] = { ... };

二十二、std::call_once / folly::once_flag — 线程安全单次初始化

是什么std::call_once + std::once_flag(或 folly 的版本)
保证一段初始化代码在多线程环境中只执行一次,即使多个线程同时到达。
执行完成后其他线程直接继续,不需要每次都加锁检查。

解决什么问题:全局资源(文件系统注册、连接器注册、函数库初始化)
需要懒初始化(第一次用时才初始化,而非程序启动就初始化),
且多个线程可能同时触发初始化。
用普通 mutex + bool flag 容易写出 double-checked locking 的 bug,
call_once 提供了无 bug 的标准实现。

Velox 例子velox/common/file/FileSystems.cpp:79velox/connectors/hive/iceberg/IcebergConnector.cpp:33

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 本地文件系统:第一次调用时注册,之后跳过
folly::once_flag localFSInstantiationFlag;

void registerLocalFileSystem(const Config& config) {
folly::call_once(localFSInstantiationFlag, [&config]() {
// 只执行一次,线程安全
FileSystemRegistry::getInstance().registerFileSystem(
"file", std::make_shared<LocalFileSystem>(config));
});
}

// Iceberg Connector 函数注册:多模块可能同时调用,只注册一次
void registerIcebergFunctions(const std::string& prefix) {
static std::once_flag registerFlag;
std::call_once(registerFlag, [&prefix]() {
registerIcebergTableHandle(prefix);
registerIcebergSplitReader(prefix);
});
}

二十三、Template Template Parameters — 类型变换框架

是什么:模板的模板参数(Template Template Parameter)允许把一个模板本身
(而非模板的某个实例)作为参数传递。
语法:template <template <typename> class Container>
Container 本身是一个接受一个类型参数的模板,而非某个具体类型。

解决什么问题tuple_xform 需要对 tuple 中的每种类型应用同一个变换模板
(比如把 tuple<int, string> 变成 tuple<Optional<int>, Optional<string>>),
但变换本身是参数化的(可以是 Optionalshared_ptrNullable 等)。
用 template template parameter,一套代码处理任意变换,不需要为每种变换写特化。

Velox 例子velox/core/Metaprogramming.h:30

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// tuple_xform<Transformer, Tuple>:对 Tuple 中每种类型应用 Transformer
template <template <typename> class Transformer, typename E>
struct tuple_xform {}; // 主模板(不可用)

// 偏特化:只匹配 std::tuple<...>,对每个元素应用 Transformer
template <template <typename> class Transformer, typename... E>
struct tuple_xform<Transformer, std::tuple<E...>> {
using type = std::tuple<typename Transformer<E>::type...>;
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// 包展开:对每个 E 应用 Transformer<E>::type
};

// 示例:把 tuple<int, string> 变成 tuple<Optional<int>, Optional<string>>
template <typename T>
struct MakeOptional { using type = std::optional<T>; };

using Original = std::tuple<int, std::string, double>;
using Optionals = tuple_xform<MakeOptional, Original>::type;
// Optionals = std::tuple<std::optional<int>, std::optional<std::string>, std::optional<double>>

这个模式在 UDF 签名反射中大量使用:把函数参数类型列表变换为对应的”可空”类型列表。


二十四、Hidden Friend Idiom — 限制 ADL 查找范围

是什么:Hidden Friend(隐藏友元)是把非成员函数作为友元定义在类体内部。
这样的函数只能通过 ADL(Argument Dependent Lookup,依赖参数的名字查找)找到,
不能通过普通限定名查找(velox::operator<< 这样调用会失败)。
作用:避免运算符进入全局命名空间污染,减少意外的隐式转换触发。

解决什么问题:如果 operator== 是普通非成员函数,
任何可以隐式转换为 StringView 的类型都会匹配,可能产生意外的比较。
Hidden Friend 确保 operator== 只有在参数类型恰好是 StringView 时才被 ADL 找到。

Velox 例子velox/type/StringView.h:171velox/common/base/Status.h:225

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct StringView {
// 定义在类体内:只有 StringView 的 operator<< 场景才能找到它
friend std::ostream& operator<<(std::ostream& os, const StringView& sv) {
os.write(sv.data(), sv.size());
return os;
}
// operator== 同理,定义在类体内,不污染全局命名空间
};

// Status 也用这个模式
class Status {
friend std::ostream& operator<<(std::ostream& ss, const Status& s) {
return ss << s.toString(); // 委托给成员函数
}
};

二十五、= delete 删除危险重载 — 主动防御

是什么:在 C++11 中,= delete 可以显式删除某个函数重载(包括构造函数、
赋值运算符、普通函数),使该重载在被调用时产生编译期错误,而不是被其他重载静默匹配。

解决什么问题StringView 不拥有数据,如果从临时 std::string 构造,
string 析构后 StringView 持有悬空指针。
编译器不会自动阻止这种用法(临时对象可以绑定到 const 引用),
= delete 主动封堵这个漏洞,把运行时的 use-after-free 提升为编译时错误。

Velox 例子velox/type/StringView.h:129

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct StringView {
// 允许从 const string& 构造(string 生命期由调用方保证)
explicit StringView(const std::string& value)
: StringView(value.data(), value.size()) {}

// 禁止从右值 string 构造:string 临时对象会立即析构,导致悬空指针
explicit StringView(std::string&& value) = delete;

// 同样禁止从右值 fbstring 构造
explicit StringView(folly::fbstring&& value) = delete;

// data() 也只允许在左值上调用(& 修饰符)
const char* data() const&& = delete; // 右值 StringView 调 data() 也是 bug
const char* data() const& { ... } // 只有左值才能调
};

// 编译器会拒绝:
StringView sv = std::string("hello"); // error: use of deleted function
auto ptr = StringView("world").data(); // error: use of deleted function

这种”主动删除危险重载”的技巧在 Velox 里形成了一种防御性编程文化:
让错误在编译期暴露,而不是在运行时留下隐患。


二十六、条件变量 (condition_variable) — 等待状态变化

是什么std::condition_variable 是 C++ 的线程等待原语,
允许一个线程在某个条件不满足时挂起(释放 CPU),
由另一个线程改变条件后调用 notify_one() / notify_all() 将其唤醒。
标准用法是配合 std::unique_lock<std::mutex> 使用:

1
mutex + condition_variable + 状态变量  =  三件套,缺一不可

解决什么问题:线程等待资源时有三种策略:

  1. 忙等(busy-wait)while (!ready) {} — 100% CPU,极度浪费
  2. 定时睡眠sleep_for(1ms) — 要么响应慢,要么仍然空转
  3. 条件变量:精确地”等到条件成立再醒”,不占 CPU,响应及时

条件变量还自带一个重要细节:wait(lock, predicate) 会在 notify
重新检查 predicate,防止虚假唤醒(spurious wakeup)——
操作系统有时会无故唤醒等待线程,predicate 是第二道门。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 标准三件套
std::mutex mu;
std::condition_variable cv;
bool ready = false;

// 等待方
std::unique_lock lock(mu);
cv.wait(lock, [&] { return ready; }); // 等到 ready==true,期间不占 CPU

// 通知方
{
std::lock_guard lock(mu);
ready = true;
}
cv.notify_one(); // 唤醒一个等待线程

Velox 中的应用

① Semaphore — 计数信号量velox/common/base/Semaphore.h:37

Velox 用 condition_variable 实现了一个计数信号量(C++20 才有标准的 std::counting_semaphore):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Semaphore {
public:
void acquire() {
std::unique_lock lock(mutex_);
if (count_) {
--count_;
} else {
++numWaiting_;
// predicate:等到 count > 0,防止虚假唤醒
cv_.wait(lock, [&] { return count_ > 0; });
--numWaiting_;
--count_;
}
}

void release() {
std::lock_guard lock(mutex_);
++count_;
if (numWaiting_ > 0) {
cv_.notify_one(); // 只有有人在等才唤醒,省去无用的 notify
}
}

private:
std::mutex mutex_;
std::condition_variable cv_;
int32_t count_;
int32_t numWaiting_{0};
};

② ExecutorBarrier — 等待所有并发任务完成velox/dwio/common/ExecutorBarrier.h:60

IO 层并行读取多个 stripe / column chunk,需要等所有子任务都完成才能继续。
ExecutorBarrier 包装 folly::Executor,每提交一个任务 count_++
完成时 count_--,都降到 0 时唤醒等待方:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class ExecutorBarrier : public folly::Executor {
public:
void waitAll() {
std::unique_lock lock(mutex_);
cv_.wait(lock, [&] { return count_ == 0; }); // 等所有任务完成
if (exception_.has_exception_ptr()) {
exception_.throw_exception(); // 汇总任何子任务抛出的异常
}
}

~ExecutorBarrier() override {
std::unique_lock lock(mutex_);
cv_.wait(lock, [&] { return count_ == 0; }); // 析构也要等,防止 UAF
}

private:
size_t count_;
std::mutex mutex_;
std::condition_variable cv_;
folly::exception_wrapper exception_;
};

③ CachedFactory — 防止重复生成velox/common/caching/CachedFactory.h:398

多线程同时 miss 同一个缓存键时,只让第一个线程去生成,其余线程等待它完成后
直接读缓存,避免重复昂贵的生成操作:

1
2
3
4
5
6
7
8
9
10
11
12
// 第一个线程:标记 key 正在生成
pending_.insert(key);
// ... 生成并写入缓存 ...
removePending(key);
pendingCv_.notify_all(); // 通知所有等待者

// 后续线程:key 已在 pending,等待
if (pending_.contains(key)) {
pendingCv_.wait(pendingLock, [&] { return !pending_.contains(key); });
// 现在缓存里有了,直接读
return getCache(key);
}

二十七、folly Future / Promise — 异步值的生产与消费

是什么:Promise / Future 是一对对象,共同表示”一个未来才会有的值”:

  • Promise:生产者持有,用来”兑现”承诺(setValue(v)setException(e)
  • Future:消费者持有,代表那个未来的值,可以链式注册回调(.thenValue().thenError()

folly 的实现比标准库的 std::future/promise 更强大:

  • **folly::SemiFuture<T>**:没有绑定 executor 的 future,可以跨线程传递,
    需要 .via(executor) 才能链接回调(防止在错误线程上执行回调)
  • **folly::Future<T>**:已绑定 executor,可以直接 .thenValue() 链式调用
  • **folly::SharedPromise<T>**:可以有多个 Future 消费同一个值(多播),
    普通 Promise 只能有一个 Future

解决什么问题

方案 问题
线程 + std::future future::get() 阻塞调用线程,浪费线程资源
回调函数 错误传播困难,多层嵌套形成”回调地狱”
condition_variable 必须持有锁等待,代码复杂,容易死锁
folly Future/Promise 非阻塞、可组合、异常自动沿链传播、executor 解耦执行位置

Velox 中的应用

① VeloxPromise — 带生命期追踪的 Promisevelox/common/future/VeloxPromise.h

Velox 包装了 folly::Promise,在析构时检查是否未兑现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
template <class T>
class VeloxPromise : public folly::Promise<T> {
public:
explicit VeloxPromise(const std::string& context)
: folly::Promise<T>(), context_(context) {}

~VeloxPromise() {
// 未兑现的 Promise 被销毁 = 消费方永远等不到值 = 程序死锁或 bug
if (!this->isFulfilled()) {
LOG(WARNING) << "PROMISE: Unfulfilled promise is being deleted. Context: "
<< context_;
}
}

private:
std::string context_; // 调试用:记录 Promise 在哪里创建的
};

// 便捷类型别名:执行引擎中最常用的 Promise/Future 对
using ContinuePromise = VeloxPromise<folly::Unit>; // 不携带值,只是信号
using ContinueFuture = folly::SemiFuture<folly::Unit>;

// 一次性创建互联的 Promise + Future
auto [promise, future] = makeVeloxContinuePromiseContract("HashBuild::barrier");

② Operator::isBlocked — 非阻塞挂起协议velox/exec/Operator.h:284

这是 Velox 执行引擎最核心的 Future 应用。
算子(Operator)不能在自己内部阻塞线程(那会卡住整个 Driver 线程),
而是通过返回 ContinueFuture 告诉调度层”我现在无法继续,等这个 Future 完成再调我”:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 每个算子都实现这个接口
virtual BlockingReason isBlocked(ContinueFuture* future) = 0;

// 示例:TableWriter 等待后端 IO flush 完成
BlockingReason TableWriter::isBlocked(ContinueFuture* future) {
if (dataSink_ && dataSink_->isBlocked()) {
// 把 Future 写出去给 Driver,Driver 挂起本 Driver 线程,不阻塞
*future = dataSink_->blockingFuture();
return BlockingReason::kWaitForIO;
}
return BlockingReason::kNotBlocked;
}

// Driver 调度循环(简化):
while (!op->isFinished()) {
ContinueFuture future;
auto reason = op->isBlocked(&future);
if (reason != BlockingReason::kNotBlocked) {
// 不阻塞线程,把 future 交给线程池的 event loop
// future 完成时重新调度本 Driver
std::move(future).via(executor).thenValue([driver] {
driver->enqueue(); // 重新入队
});
return; // Driver 线程立即释放,去做别的事
}
op->getOutput();
}

③ Task 状态变化通知velox/exec/Task.h:305

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Task {
public:
// 等待 Task 完成:不阻塞,返回 Future
ContinueFuture taskCompletionFuture();

// 等待 Task 进入某状态(用于测试和协调)
ContinueFuture stateChangeFuture(uint64_t maxWaitMicros);

// 请求暂停所有 Driver:返回 Future,完成时所有 Driver 已暂停
ContinueFuture requestPause();

// 请求终止,等待所有线程退出
ContinueFuture terminate(TaskState terminalState);
};

// 使用方:不阻塞主线程,完成后回调
task->requestPause()
.via(folly::getGlobalCPUExecutor())
.thenValue([](auto&&) {
// 所有 Driver 已暂停,可以安全做全局操作(如 spill)
});

④ AsyncDataCache — SharedPromise 多播缓存未命中velox/common/caching/AsyncDataCache.h:330

多个读线程同时请求同一个未缓存的 block,第一个发起 IO,
后续的拿到 SemiFuture,等第一个 IO 完成后一起继续:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// CacheEntry:正在加载时设置 SharedPromise,所有等待者拿到对应 Future
folly::SemiFuture<bool> CacheEntry::getFutureLocked() {
if (promise_ == nullptr) {
// SharedPromise 允许多个消费者:N 个 getFuture() 都能被同一个 setValue 唤醒
promise_ = std::make_unique<folly::SharedPromise<bool>>();
}
return promise_->getSemiFuture();
}

// IO 完成时:一次 setValue 唤醒所有等待者
promise_->setValue(true);

// 异步文件 IO:preadvAsync 返回 SemiFuture
virtual folly::SemiFuture<uint64_t> preadvAsync(
uint64_t offset,
const std::vector<folly::Range<char*>>& buffers) const {
// 默认实现:同步包装成 Future(S3/GCS 实现是真正的异步)
return folly::SemiFuture<uint64_t>(preadv(offset, buffers));
}

condition_variable vs Future/Promise 的选型

场景 选哪个
等待”计数归零”(Barrier、Semaphore) condition_variable
等待”一次性状态变化”,不需要回调链 condition_variable
算子挂起后需要重新调度(不阻塞线程) ContinueFuture
异步 IO,完成后继续处理数据 SemiFuture
多个消费者等同一个值(缓存加载) folly::SharedPromise
需要组合多个异步操作(链式处理) folly::Future + .thenValue()

二十八、类的特殊成员函数设计 — Rule of Zero/Three/Five

是什么:每个 C++ 类有六个”特殊成员函数”,编译器在需要时会自动生成:

  • 默认构造函数 T()
  • 析构函数 ~T()
  • 拷贝构造 T(const T&)
  • 拷贝赋值 T& operator=(const T&)
  • 移动构造 T(T&&)
  • 移动赋值 T& operator=(T&&)

三条经验法则:

  • Rule of Zero(零法则):如果类的成员都能自管理资源(如 std::vectorshared_ptr),
    就什么都别写,让编译器全自动生成。这是首选
  • Rule of Three(三法则):如果你需要手写析构/拷贝构造/拷贝赋值之一(通常因为管理裸资源),
    那三个都得写——否则另两个的默认实现会出错(浅拷贝导致 double-free)。
  • Rule of Five(五法则):C++11 后,写了上述三个,通常还要补上移动构造和移动赋值,
    否则移动操作会退化成拷贝,丢失性能。

关键规则:一旦你声明了析构函数(或拷贝操作),编译器就不再自动生成移动操作。
这意味着写了 ~T() 却忘了移动操作,对象就只能拷贝,不能移动——一个隐蔽的性能陷阱。

28.1 为什么禁止拷贝和移动 (= delete)

解决什么问题:有些对象代表”独一无二的执行实体”——它们持有线程、锁、
或被其他对象用裸指针/引用指向。这类对象一旦被拷贝或移动,
就会出现”两个对象管同一个资源”或”指向它的指针失效”的灾难。
显式 = delete 把这种误用变成编译错误。

Velox 例子velox/exec/Driver.h:357

1
2
3
4
5
6
7
8
9
10
11
12
// Driver 代表一个执行线程的状态机,被 Task 用 shared_ptr 持有,
// 被 Operator 用裸指针反向引用。绝不能拷贝或移动。
class Driver : public std::enable_shared_from_this<Driver> {
public:
~Driver();

// 四个一起删除:拷贝构造、拷贝赋值、移动构造、移动赋值
Driver(const Driver&) = delete;
Driver& operator=(const Driver&) = delete;
Driver(Driver&&) = delete;
Driver& operator=(Driver&&) = delete;
};

为什么 Driver 必须禁止这些:

  1. 被裸指针引用:每个 Operator 持有 Driver*,移动 Driver 会让这些指针悬空。
  2. 持有运行时状态:Driver 内有线程归属、阻塞状态、CPU 计时,拷贝它没有任何语义意义。
  3. 生命期由 shared_ptr 统一管理:Driver 只能通过 shared_from_this() 传递,
    值拷贝会破坏引用计数模型。

注意:只要声明= delete 的移动操作,类就彻底不可移动。
如果只想”不可拷贝但可移动”(如 unique_ptr 语义),就只删拷贝、留移动。

28.2 为什么有的类不需要写析构函数

解决什么问题:很多类遵循 Rule of Zero——成员全是能自管理的类型
std::vectorstd::stringshared_ptrstd::optional、值类型),
对象销毁时编译器生成的默认析构会自动、按逆序析构每个成员,无需手写。
手写一个空的 ~T() {} 反而有害:它会抑制移动操作的自动生成,让类退化为只能拷贝。

Velox 例子CursorParametersvelox/exec/Cursor.h:26)是一个纯数据聚合体:

1
2
3
4
5
6
7
8
9
10
11
// 没有任何特殊成员函数声明 —— 完全的 Rule of Zero
struct CursorParameters {
std::shared_ptr<const core::PlanNode> planNode; // shared_ptr 自管理
int32_t destination{0}; // 值类型
std::shared_ptr<core::QueryCtx> queryCtx; // shared_ptr 自管理
std::string spillDirectory = ""; // string 自管理
std::function<std::string()> spillDirectoryCallback = nullptr;
std::unordered_map<std::string, std::string> queryConfigs = {};
// ... 全是自管理成员
};
// 析构、拷贝、移动全部由编译器生成且正确。一行都不用写。

StringViewvelox/type/StringView.h)也没有析构函数——
它是非拥有视图,16 字节全是平凡数据(int32_t + 两段 char),
默认析构什么都不做,正是我们想要的。

28.3 为什么有的类必须定义虚析构函数

是什么:当一个类作为多态基类(会通过基类指针 delete 派生对象)时,
**析构函数必须是 virtual**。否则 delete basePtr 只会调用基类析构,
派生类的析构被跳过——派生类成员泄漏,行为未定义。

判断标准

  • 有虚函数 / 会被继承 / 通过基类指针删除 → 必须 virtual ~T()
  • 值类型 / 不会被继承 → 不要写 virtual(虚函数表指针会让对象变大、不能放进向量)

Velox 例子

基类要虚析构——velox/common/memory/MemoryPool.h:181

1
2
3
4
5
6
7
8
// MemoryPool 是抽象基类,MemoryPoolImpl 派生它,且通过基类 shared_ptr 删除
class MemoryPool : public std::enable_shared_from_this<MemoryPool> {
public:
virtual ~MemoryPool(); // 必须 virtual,否则 MemoryPoolImpl 析构被跳过
virtual int64_t capacity() const = 0; // 有纯虚函数 = 注定被继承
};

class MemoryPoolImpl : public MemoryPool { ... }; // 派生类

velox/buffer/Buffer.h:65Buffer 同理:

1
2
3
4
5
6
7
8
9
class Buffer {
public:
virtual ~Buffer() = default; // BufferView/AlignedBuffer 派生它
virtual void releaseResources() {} // 派生类各自实现资源释放
};

class BufferView : public Buffer {
~BufferView() override { ... } // override 显式标注覆盖
};

反例——TaskCursorvelox/exec/Cursor.h:139)作为接口基类,
虚析构是 default 但必须存在:

1
2
3
4
5
class TaskCursor {
public:
virtual ~TaskCursor() = default; // 接口类:派生实现通过基类 unique_ptr 删除
virtual bool moveNext() = 0; // 纯虚 = 接口
};

二十九、shared_ptr / weak_ptr — 引用计数与打破循环

29.1 两种计数器:强引用 vs 弱引用

是什么std::shared_ptr<T> 通过引用计数管理对象生命期。
它的”控制块”(control block)里其实有两个计数器:

计数器 含义 谁增减
strong count(强引用数) 有多少 shared_ptr 拥有对象 shared_ptr 拷贝 +1,析构 -1
weak count(弱引用数) 有多少 weak_ptr 观察对象 weak_ptr 拷贝 +1,析构 -1

生命期规则:

  • strong count 归 0对象本身被析构(调用 ~T(),释放 T 占的内存)
  • strong count 和 weak count 都归 0控制块也被释放

std::weak_ptr<T> 是一个不拥有对象的弱引用:它不增加 strong count,
因此不会阻止对象被销毁。它只能观察对象”是否还活着”,
通过 .lock() 尝试提升为 shared_ptr

为什么需要 weak_ptr——打破循环引用
如果对象 A 用 shared_ptr 持有 B,B 又用 shared_ptr 持有 A,
两者的 strong count 永远 ≥ 1,谁都无法归零,内存永久泄漏
把其中一个方向改成 weak_ptr,循环就被打破。

29.2 .lock() 的提升技巧:原子地”续命”

是什么weak_ptr::lock() 是核心操作:它原子地检查对象是否还活着,
如果活着就返回一个新的 shared_ptr(strong count +1),保证在你使用期间对象不被销毁;
如果对象已死(strong count 已归 0),返回空 shared_ptr

这个”检查 + 提升”是原子的——不会出现”刚检查还活着,下一行就被别的线程销毁”的竞态。
这是异步、多线程环境下安全访问可能已销毁对象的标准手法。

1
2
3
4
5
6
7
std::weak_ptr<T> weak = ...;
if (auto strong = weak.lock()) { // 原子提升
strong->doWork(); // 此作用域内对象保证存活
} // strong 析构,strong count -1
else {
// 对象已被销毁,安全地放弃
}

29.3 Velox 应用:异步回调持有 weak_ptr,回调内 lock 续命

这是 Velox 处理”异步任务可能比对象活得久”的核心模式。

例子:SpillMerger 的异步流读取velox/exec/Merge.cpp:644

SpillMerger 把多个溢写文件流并行读入。每次读取是异步的,
通过 folly::Future::thenValue 链式调度下一次读取。
问题:如果 SpillMerger 在某次异步读取还排队时就被销毁了,回调访问 this 就是 use-after-free。

解法:每个异步回调捕获 weak_ptr<SpillMerger>(命名为 mergeHolder),
回调开始时 lock()——活着才继续,死了就放弃:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class SpillMerger : public std::enable_shared_from_this<SpillMerger> {
void readFromSpillFileStream(
const std::weak_ptr<SpillMerger>& mergeHolder, // 弱引用,不延长生命期
size_t streamIdx) {
// 关键:lock() 原子提升。若 SpillMerger 已析构,merger == nullptr
const auto merger = mergeHolder.lock();
if (merger == nullptr) {
LOG(ERROR) << "SpillMerger is destroyed, abandon reading from batch stream";
return; // 对象已死,安全退出,不碰 this
}
// merger 在此作用域内持有强引用,保证 this 存活

// ... 读一批数据 ...

// 异步调度下一次读取:回调继续捕获 weak_ptr,而非 shared_ptr
std::move(future)
.via(executor_)
.thenValue([this, mergeHolder, streamIdx](auto&&) {
readFromSpillFileStream(mergeHolder, streamIdx); // 递归续读
});
}

void scheduleAsyncSpillFileStreamReads() {
for (auto i = 0; i < batchStreams_.size(); ++i) {
executor_->add([&, streamIdx = i]() {
// 从 this 拿 weak_ptr:shared_from_this() 是 shared,再包成 weak
readFromSpillFileStream(std::weak_ptr(shared_from_this()), streamIdx);
});
}
}
};

**为什么捕获 weak_ptr 而非 shared_ptr**:
如果异步回调捕获 shared_ptr<SpillMerger>,那么只要还有回调在排队,
strong count 就 ≥ 1,SpillMerger 永远不会被销毁——即使查询已经取消。
弱引用让 SpillMerger 的生命期由其真正的所有者决定,
异步回调只是”有机会就续读,没机会就退出”的旁观者。

**例子:Task::MemoryReclaimer 持有 weak_ptr**(velox/exec/Task.h:973

内存仲裁器可能在 Task 已结束后才触发回收。
Task 的 MemoryPool 可能比 Task 本身活得久(注释明确写了这一点),
所以 reclaimer 只能弱引用 Task:

1
2
3
4
5
6
7
8
9
10
11
class Task::MemoryReclaimer : public exec::MemoryReclaimer {
private:
// 用 weak_ptr 而非 shared_ptr:避免 reclaimer 反向延长 Task 生命期
// (否则 Task 和它的 MemoryPool 会互相持有 → 循环引用)
std::weak_ptr<Task> task_;

// 回收操作开始时 lock(),确保回收期间 Task 不被销毁
std::shared_ptr<Task> ensureTask() const {
return task_.lock(); // Task 已死则返回 nullptr,回收安全跳过
}
};

例子:Operator 用 weak_ptr 反向引用velox/exec/Operator.h:570

Driver 用 shared_ptr 拥有它的 Operator 链;
Operator 需要反向访问 Driver,但绝不能用 shared_ptr(否则 Driver↔Operator 循环引用):

1
2
3
4
5
6
7
class OperatorCtx {
std::shared_ptr<Driver> driver() const {
return driver_.lock(); // 提升后使用,用完释放
}
private:
const std::weak_ptr<Driver> driver_; // 反向引用必须是弱引用
};

模式总结:Velox 中 weak_ptr 的两大用途——

  1. 打破所有权循环Operator → DriverTask → MemoryPool → Reclaimer → Task 等反向边用 weak。
  2. 异步回调的生命期解耦:回调捕获 weak,执行时 lock(),对象活着才干活,死了就放弃,
    既不延长对象生命期,又避免 use-after-free。

三十、boost::intrusive_ptr — 侵入式引用计数

是什么std::shared_ptr 把引用计数放在一个独立的控制块
(对象本体之外,额外一次堆分配,计数和对象不在一起)。
侵入式引用计数(intrusive refcount)则把计数器直接做成对象的成员,
boost::intrusive_ptr 通过两个全局函数 intrusive_ptr_add_ref / intrusive_ptr_release
增减这个内嵌计数。

解决什么问题

  • 省一次分配shared_ptr 的控制块是单独 new 的(除非用 make_shared),
    侵入式计数和对象同体,无额外分配。
  • 更小的指针intrusive_ptr 只有一个裸指针大小(8 字节),
    shared_ptr 是两个指针(16 字节:对象指针 + 控制块指针)。
  • 任意指针互转:可以从裸 T* 直接重建 intrusive_ptr(计数在对象内),
    shared_ptr 做不到(控制块信息丢失)。

Buffer 是 Velox 最高频分配的对象(每个向量都持有若干 Buffer),
这点开销差异在亿级向量场景下非常可观,所以选择侵入式计数。

Velox 例子velox/buffer/Buffer.h:54

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// BufferPtr 就是 intrusive_ptr,不是 shared_ptr
using BufferPtr = boost::intrusive_ptr<Buffer>;

class Buffer {
public:
virtual ~Buffer() = default;

// 计数器内嵌在对象里,原子操作(acq_rel 保证多线程可见性)
void addRef() noexcept {
referenceCount_.fetch_add(1, std::memory_order_acq_rel);
}

void release() {
// fetch_sub 返回旧值;旧值为 1 说明这是最后一个引用 → 释放
if (referenceCount_.fetch_sub(1, std::memory_order_acq_rel) == 1) {
releaseResources();
// 归还内存给 pool ...
}
}

private:
std::atomic_int32_t referenceCount_{0}; // 计数器是对象的成员
};

// intrusive_ptr 通过这两个 ADL 全局函数驱动计数(boost 约定的接口)
FOLLY_ALWAYS_INLINE void intrusive_ptr_add_ref(Buffer* buffer) noexcept {
buffer->addRef();
}
FOLLY_ALWAYS_INLINE void intrusive_ptr_release(Buffer* buffer) noexcept {
buffer->release();
}

shared_ptr vs intrusive_ptr 选型

维度 std::shared_ptr boost::intrusive_ptr
计数位置 独立控制块(对象外) 内嵌对象成员
指针大小 16 字节(两指针) 8 字节(一指针)
额外分配 有(除非 make_shared)
支持 weak_ptr 支持 不支持(无 weak 计数)
裸指针重建 不能
Velox 用途 Task/Driver/Pool 等需要 weak 的对象 Buffer 等超高频、不需要 weak 的对象

这正体现了 Velox 的工程权衡:需要 weak_ptr(打破循环、异步续命)的地方用 shared_ptr
超高频分配、不需要弱引用的 Bufferintrusive_ptr 省掉每次分配和一半指针体积。


小结

按本文章节顺序汇总:

# 维度 核心技术 收益
模板元编程 SFINAE/void_tif constexproverloaded、具名 NTTP、全/偏特化、函数不可偏特化的应对 零运行时开销的可选接口与类型分发
类型系统 TypeKind + TypeTraits 特化、DECLARE_CONDITIONAL_TYPE_NAME 运行时与编译期类型双轨并行
内存管理 MemoryPool 树、raw_vector、Scratch、STL Allocator 适配 可追踪、可限制、缓存友好的分配
错误处理 分层异常、[[noreturn]] out-of-line、DCHECK 热路径不因错误检查膨胀
SIMD 与向量化 xsimd、AVX2 intrinsics、SelectivityVector 批处理吞吐量提升 4-16x
性能工程 FOLLY_ALWAYS_INLINELIKELY、移动语义、constexpr 常量 每个 ns 都不浪费
Folly 库 Synchronized、F14、Executordynamic 生产级并发与容器
设计模式 Builder + optional、Visitor、Strategy、Registry 扩展性与可维护性
向量化执行 多级编码 Vector、DecodedVectorLazyVector 列式、延迟、零拷贝执行
Arrow 集成 C Data Interface、buffer 所有权转移 引擎间零拷贝互通
十一 并发设计 原子变量、shared-nothing Driver 模型 简单高效的并发设计
十二 CRTP 静态多态、可复用 Builder 基类 链式 setter 零虚函数开销
十三 Policy-based Design constexpr Policy struct 模板参数 编译期行为注入,消除运行时分支
十四 StringView 16 字节 SSO + 前缀缓存 字符串比较减少堆访问
十五 folly::Expected<T,E> 类型安全错误传播 无异常开销的显式错误处理
十六 [[nodiscard]] 类级别 nodiscard 属性 编译期强制检查返回值
十七 folly::Range 非拥有数组视图 替代裸指针对,接口安全
十八 结构化绑定 auto [a, b] = ... 多返回值语义清晰
十九 noexcept 移动构造/析构标注 允许 vector 用移动而非拷贝
二十 类型双关 reinterpret_cast 位模式操作 哈希与序列化的底层位操作
二十一 constexpr 查找表 编译期查找表放 .rodata 以空间换时间,cache 友好
二十二 call_once folly::once_flag 懒初始化 线程安全的单次全局初始化
二十三 Template Template Parameters 模板的模板参数 类型变换框架,UDF 签名反射
二十四 Hidden Friend 类体内定义友元运算符 限制 ADL 范围,避免全局污染
二十五 = delete 显式删除危险重载 把运行时 UAF 提升为编译错误
二十六 条件变量 Semaphore、ExecutorBarrier、CachedFactory 精确等待条件成立,不空转
二十七 Future / Promise ContinueFutureisBlockedSharedPromise 非阻塞挂起与异步 IO 协调
二十八 特殊成员函数 Rule of Zero/Three/Five、Driver 删除拷贝移动 正确管理资源生命期
二十九 shared_ptr / weak_ptr 强/弱双计数、.lock() 续命、打破循环 异步回调生命期解耦,防 UAF
三十 intrusive_ptr Buffer 侵入式计数 省一次分配、半个指针体积

Velox 的核心设计哲学:一切能在编译期决定的,绝不拖到运行时;一切能用位操作或 SIMD 完成的,绝不逐个元素循环。