在向量化执行系统中,表达式构建是不可或缺的基础环节。无论是 SQL 中的投影、筛选,还是分区、聚合、排序,最终都需转化为底层执行引擎能识别和执行的表达式树。而在 Apache Cloudberry 向量化执行框架中,这一过程由 Gandiva 表达式引擎负责完成。
随着数据规模与查询复杂度的提升,我们逐渐意识到,表达式构建本身正成为影响执行性能的关键路径之一。特别是在高并发、多表达式拼接的场景下,构建过程的性能瓶颈愈加突出。本文将结合实际优化案例,分享我们如何识别问题、设计优化方案,并用火焰图验证成效。
为何选 Gandiva?JIT + Arrow 的组合拳
Gandiva 是 Apache Arrow 项目中的子模块,它基于 LLVM 构建 JIT 编译能力,专为高性能、批量化的列式计算而设计。我们选择 Gandiva 作为表达式引擎的主要原因有三点:
- 向量化执行友好:Gandiva 表达式以 Arrow RecordBatch 为输入/输出单位,与 Cloudberry 的内存格式完全兼容,避免额外序列化/反序列化开销。
- JIT 编译能力强:Gandiva 支持将表达式编译为本地机器码,执行效率显著优于解释执行。
- 表达式树抽象清晰:其表达式结构基于语法树(AST),便于分析、合并、转换、优化。
但“强大”背后也隐藏着一个问题:表达式构建过程并非“零成本”,尤其在表达式数量和深度快速增长时,构建开销成为了不容忽视的负担。
原始构建路径的问题:节点重复 & 树结构过深
在未优化前,我们采用“逐表达式构建”的方式——每处理一条 SQL 表达式,就从头创建一棵新的表达式树。这种策略在简单查询下运行良好,但在复杂嵌套查询、窗口函数、联表计算等场景下暴露出以下问题:
- 公共子表达式重复构建:同一表达式片段(如 lower(colA))在不同上下文中多次出现时,每次都重新生成节点,造成冗余。
- 表达式树结构深且复杂:表达式链条变长时,嵌套层级加深,构建耗时近似呈线性增长。
- Hash 逻辑不稳定:相同表达式结构,由于构建路径差异导致节点 hash 不一致,影响缓存和优化判断。
我们对典型查询的表达式构建过程进行了耗时统计,结果显示:
- 在包含 20+ 表达式的复杂查询中,表达式构建耗时占整体查询时间的 10%~15%;
- 其中约 40% 的表达式为可复用的子表达式,但未被有效识别与复用;
- 构建阶段的所有开销几乎全部集中在 on-CPU 路径上,火焰图显示 CreateExpressionNode、ToArrowNode 等函数在 CPU 调用栈中占比极高,成为构建瓶颈的主要耗时点。
这些现象表明:表达式构建过程不仅费时,而且浪费资源。
优化策略:公共子表达式识别 + 哈希原子化
我们采用两项优化手段来重构表达式构建路径:
- 公共子表达式识别(CSE)
引入表达式 DAG 结构,在构建过程中为每个子表达式生成唯一 key(基于语义签名),并放入全局表达式缓存池。后续若再次请求相同表达式,直接复用已有子树。
- 优点:减少冗余节点构建,降低构建深度;
- 技术点:等价表达式归一化(如 a + b vs b + a)、表达式 hash 去重。
- 哈希表达式原子化
将每一个表达式节点封装为具有确定性 hash 的原子单元,避免因构建路径差异导致 hash 冲突。统一采用 结构 hash + 类型信息 + 参数签名 的组合哈希策略,确保缓存命中率提升。
优化后,我们实现了表达式构建路径的“结构性去重”:从构建“树”转为拼装“块”,如搭积木般复用构建单元,降低系统负担。
优化效果对比:结构简化 & 构建耗时下降
通过对比优化前后在复杂 SQL 下的表达式构建过程,我们观察到以下显著变化:
不仅节点数量明显下降,构建时间也随之降低了50%以上,特别是在复合查询中表现尤为明显。
火焰图验证:构建路径 on-CPU 时间显著下降
我们进一步通过 perf 工具配合火焰图对比优化前后的 CPU 使用情况,焦点集中在表达式构建阶段。
优化前的火焰图中,Gandiva::TreeExprBuilder::MakeExpression() 及其内部调用占据主火焰图的 30% 高度,显著吞噬 on‑CPU 资源。
优化后,火焰图中该函数堆栈深度显著缩减,仅占主图不到 10%,并可见更多时间释放给后续执行逻辑,如 Eval、Filter、Project 等。
这说明:表达式构建从 CPU 消耗的“主角”,退回到了其应有的“配角”角色。
表达式构建常被认为是“编译期行为”,但在现代向量化系统中,它的性能表现直接影响执行链路的起跑速度。
通过本次优化,我们验证了如下几点:
- 表达式构建本身具有显著的优化空间;
- 结构性去重 比单纯加快构建速度更有效;
- 可观测性工具(如火焰图) 是评估优化效果的关键利器。
这也为后续优化其他执行环节(如重分布、调度、缓存)提供了经验模板:先观测,再定位,再结构重构。