完整教程:大数据计算引擎-全阶段代码生成(Whole-stage Code Generation)与火山模型(Volcano)对比

Whole-stage Code Generation(全阶段代码生成)是计算引擎(如 Spark、Flink、StarRocks)为减少算子交互开销、提升执行效率而设计的核心优化技术,核心逻辑是:将一段连续的、无数据 Shuffle 的算子(如 “过滤→投影→聚合”)逻辑合并成单一的、优化的机器码,替代传统 “算子逐个调用、数据在算子间频繁拷贝” 的执行方式,从而消除函数调用、数据缓冲等冗余开销。

目录

1. 传统算子调用方式(valcano)

2. Whole-stage Code Generation 方式

3. 差异总结

4. 实现对比-spark源码

4.1、Whole-stage Code Generation源码实现

生成代码的关键流程

4.2、Volcano执行模型源码实现

4.3、源码对比与执行流程

物理计划举例(代码生成与Volcano)

1. Whole-stage Codegen 的物理计划

2. Volcano 的物理计划

spark实际实现总结


以 SQL 查询 SELECT name FROM user WHERE age > 30(筛选年龄 > 30 的用户姓名)为例,对比传统算子调用与 Whole-stage Code Generation 的执行过程:

1. 传统算子调用方式(valcano)

执行流程拆分为 “过滤算子” 和 “投影算子” 两个独立步骤,中间通过缓冲传递数据:

  • 步骤 1:过滤算子处理

    1. 从存储读取原始数据(每条记录包含 agename 等字段);
    2. 逐个判断 age > 30
      • 若符合条件,将整条记录(包括 agename 等)写入中间缓冲(如内存数组);
      • 若不符合,跳过。
    3. 过滤完成后,调用 “投影算子” 的入口函数,传递缓冲地址。
  • 步骤 2:投影算子处理

    1. 从中间缓冲读取过滤后的整条记录;
    2. 从记录中提取 name 字段,写入结果缓冲
    3. 重复上述步骤,直到缓冲中所有记录处理完毕。
  • 核心开销

    • 2 次函数调用(过滤→投影);
    • 1 次完整记录的中间拷贝(从过滤缓冲到投影算子);
    • 缓冲管理开销(申请 / 释放内存、判断缓冲边界)。

2. Whole-stage Code Generation 方式

将 “过滤 + 投影” 逻辑融合为一段代码,直接编译执行:

  • 步骤 1:生成融合代码引擎分析算子逻辑后,自动生成一段合并逻辑的代码(类似伪代码):

    // 直接遍历原始数据,同时完成过滤和投影
    for (int i = 0; i < 原始数据长度; i++) {
        原始记录 = 原始数据[i];
        if (原始记录.age > 30) {  // 过滤逻辑
            结果缓冲.add(原始记录.name);  // 投影逻辑
        }
    }
  • 步骤 2:编译执行这段代码被直接编译为机器码(如 x86 指令),CPU 直接执行:

    • 遍历原始数据时,同时完成过滤和投影,无需中间缓冲;
    • 数据从原始存储读取后,直接提取 name 写入结果,无冗余拷贝。
  • 核心开销

    • 0 次算子间函数调用(逻辑合并为单循环);
    • 0 次中间数据拷贝(直接从原始数据取 name);
    • 仅一次循环遍历,无缓冲管理开销。

3. 直观差异总结

执行方式数据流动路径关键开销点效率对比(假设 10 万条数据)
传统算子调用(valcano)原始数据→过滤缓冲→投影缓冲2 次函数调用 + 1 次完整记录拷贝耗时约 10ms
Whole-stage Code Generation原始数据→结果缓冲0 函数调用 + 0 中间拷贝耗时约 2ms(提速 5 倍)

传统执行方式的痛点是 “算子间交互开销 > 计算本身开销”(如函数调用、数据拷贝、缓冲管理占比超 60%);全阶段代码生成通过 “逻辑融合 + 直接编译”,将这些冗余开销降至接近 0,尤其在 “数据量小但计算密集”(如复杂聚合、多算子串联)场景下,性能可提升 2-10 倍。

4. 实现对比-spark源码

下面是Spark SQL源码中 Whole-stage Code Generation 和 Volcano(传统迭代器模型)实现的关键源码片段,并附了详细对比说明。


4.1、Whole-stage Code Generation源码实现

Whole-stage Code Generation 的核心类是 WholeStageCodegenExec,它会为可以融合的子算子生成一大段 Java 代码。

sql/core/src/main/scala/org/apache/spark/sql/execution/WholeStageCodegenExec.scalav1

/**
 * SparkPlan A should override `doProduce()` and `doConsume()`.
 *
 * `doCodeGen()` will create a `CodeGenContext`, which will hold a list of variables for input,
 * used to generated code for [[BoundReference]].
 */

生成代码的关键流程

  • 每个可融合算子实现 doProduce 和 doConsume,拼接代码。
  • doCodeGen 方法会把所有融合算子的代码生成出来,最后用 Janino 编译。

sql/core/src/main/scala/org/apache/spark/sql/execution/WholeStageCodegenExec.scalav2

val duration = System.nanoTime() - startTime
WholeStageCodegenExec.increaseCodeGenTime(duration)
logDebug(s"\n${CodeFormatter.format(cleanedSource)}")
(ctx, cleanedSource)

4.2、Volcano执行模型源码实现

Volcano(传统迭代器模型)每个算子实现自己的 doExecute 方法,返回一个 RDD[InternalRow],本质上是迭代器链式调用。

sql/core/src/main/scala/org/apache/spark/sql/execution/objects.scala

override protected def doExecute(): RDD[InternalRow] = {
  child.execute().mapPartitionsInternal { iter =>
    // ...
    iter.map { row =>
      // 这里就是传统的每条数据逐条处理
    }

每个算子的 doExecute 都是类似的模式,上一层的 Iterator.next() 调用下一层的 Iterator.next(),形成嵌套结构。

4.3、源码对比与执行流程

执行模型关键类/方法处理流程优化点
Whole-stage CodegenWholeStageCodegenExec.doCodeGen/doProduce/doConsume代码生成:将多个算子的逻辑融合为一个批处理 Java 方法批量处理、极少对象和函数调用、易于JVM优化
VolcanoSparkPlan.doExecute + Iterator链每个算子单独实现 RDD/Iterator,层层嵌套 next逐条处理、对象和函数调用多、GC压力大

物理计划举例(代码生成与Volcano)

1. Whole-stage Codegen 的物理计划

sql/core/src/test/scala/org/apache/spark/sql/execution/WholeStageCodegenSuite.scalav2

val df = spark.range(10).filter("id = 1").selectExpr("id + 1")
val plan = df.queryExecution.executedPlan
assert(plan.exists(_.isInstanceOf[WholeStageCodegenExec]))
assert(df.collect() === Array(Row(2)))

说明: filter + project 两个算子融合在同一个 WholeStageCodegenExec 内。

2. Volcano 的物理计划

对于不支持代码生成的算子(如UDF),Spark会退回传统执行模型,每个算子都是独立的 doExecute+Iterator

4.4 spark实际实现总结

  • Whole-stage Code Generation:算子链融合、批量代码生成、性能极高。适用于 Filter、Project、Map、无分组聚合等算子,且中间没有 Exchange(shuffle)/join等阻断节点。
  • Volcano模式:每个算子个别处理、对象和函数调用多,适用于不能代码生成的场景(如UDF、复杂算子)。
  • 源码实现:SparkPlan 体系下,WSCG的 doCodeGen/doProduce/doConsume 对比 Volcano 的 doExecute + Iterator链,是两套核心执行框架。
执行方式Volcano(传统)Whole-stage Code Generation
处理粒度单条记录(Iterator next)批处理(for/while循环)
函数调用/对象开销多层嵌套、频繁调用一次性完成,极少函数/对象开销
JVM优化空间受限极大,可内联、展开、寄存器优化
性能较低极高

(欢迎关注,欢迎订阅 -)

推荐阅读 : 

大数据计算引擎-从源码看Spark AQE对于倾斜的处理

深入starrocks-怎样实现多列联合统计信息

深入starrocks-多列联合统计一致性探查与策略(YY一下)

posted @ 2025-11-14 12:20  yangykaifa  阅读(17)  评论(0)    收藏  举报