数据库即时编译JIT

📅 2026/6/29 20:20:10
数据库即时编译JIT
一句话Bytecode 是一种中间代码格式JIT 是一种把代码在运行时编译成机器码的技术。1. Bytecode 是什么Bytecode 是介于源代码和机器码之间的中间表示。例如 Javaint x a b;编译后不是直接变成 x86 / ARM 机器码而是先变成 JVM 能理解的 bytecodeiload_1 iload_2 iadd istore_3Bytecode 本身通常不能直接被 CPU 执行需要由虚拟机解释执行或者进一步编译。常见例子系统BytecodeJavaJVM bytecodePythonCPython bytecode.NETCIL / MSILSQLiteVDBE bytecodePostgreSQL表达式执行中也会有类似中间表示但不一定叫 bytecode2. JIT 是什么JIT 是Just-In-Time Compilation即时编译。它的作用是程序运行时把某些代码编译成 CPU 可以直接执行的机器码。比如 JVM 执行 Java bytecode 时开始可能解释执行JVM bytecode → JVM 解释器执行如果发现某段代码很热、执行很多次JIT 会把它编译成机器码JVM bytecode → JIT 编译 → x86 / ARM 机器码之后 CPU 就可以直接跑这段机器码速度通常更快。3. 二者核心区别对比项BytecodeJIT本质中间代码格式运行时编译技术是“什么”一种代码表示一种执行优化方式什么时候产生通常在编译前期或解释器准备阶段程序运行时谁执行虚拟机解释器或 JIT 编译后由 CPU 执行JIT 编译器负责编译CPU 执行结果机器码是否一定更快不一定热代码通常更快但有编译开销典型例子Java bytecode、Python bytecode、SQLite VDBE bytecodeJVM JIT、V8 JIT、PostgreSQL LLVM JIT4. 它们的关系Bytecode 和 JIT 经常一起出现但不是必须绑定。常见路径是源代码 ↓ Bytecode ↓ 解释执行或者源代码 ↓ Bytecode ↓ JIT 编译 ↓ 机器码 ↓ CPU 执行所以可以理解为Bytecode 是 JIT 的输入之一JIT 的输出通常是机器码。但 JIT 不一定必须从 bytecode 开始它也可以从 AST、IR、查询计划、LLVM IR 等中间表示开始。5. 放到数据库里怎么理解以数据库执行 SQL 为例Bytecode 型数据库执行比如 SQLite 会把 SQL 编译成自己的 VDBE bytecodeSELECT name FROM users WHERE age 18;大致变成OpenRead Rewind Column Gt ResultRow Next Halt然后由SQLite 的虚拟机一条一条解释执行这些 bytecode。JIT 型数据库执行PostgreSQL、ClickHouse、SingleStore 等系统可能会把查询中的表达式、过滤条件、聚合逻辑等编译成本机机器码WHERE age 18可能被生成成专门的机器码函数而不是每一行都解释执行表达式。6. 一个类比可以这样理解Bytecode 像菜谱的标准步骤。它不是食物本身也不是厨师直接的肌肉动作而是一套中间指令。解释器像厨师每次照着菜谱一步步做。JIT 像厨师发现这道菜要做一万遍于是专门训练出一套最快动作流程。以后再做同一道菜就不用每次慢慢查菜谱了。总结Bytecode 是“代码长什么样”JIT 是“代码怎么被加速执行”。更准确地说Bytecode 中间表示 Interpreter 解释执行 bytecode JIT 运行时把 bytecode/IR 编译成机器码 Machine code CPU 真正执行的代码回到顶部向量化执行Vectorized Execution和JIT的关系向量化执行Vectorized Execution和JIT都是数据库执行引擎的加速技术但它们解决问题的方式不同。一句话向量化执行是“批量解释执行”JIT 是“生成专用机器码执行”。1. 普通解释执行的问题传统 Volcano / iterator 执行模型通常是一行一行处理for each row: 取一行 判断 WHERE 条件 计算表达式 输出结果问题是每处理一行都可能产生大量函数调用、分支判断、虚函数调用、类型判断CPU 利用率不高。2. 向量化执行是什么向量化执行不是一行一行处理而是一批一批处理。例如一次处理 1024 行for each batch of 1024 rows: 批量读取 age 列 批量判断 age 18 批量计算表达式 批量输出结果它的核心思想是把数据库操作作用在一列或一批数据上而不是单行数据上。例如SELECT price * quantity FROM orders WHERE amount 100;向量化执行会更像这样amount[0..1023] 100 price[0..1023] * quantity[0..1023]这样有几个好处优点解释减少函数调用开销一次处理一批数据而不是每行调用一次更适合 CPU cache批量、连续访问列数据更适合 SIMDCPU 可以一条指令处理多个值分支预测更友好减少逐行判断带来的分支开销实现相对简单比 JIT 更容易控制和调试常见采用向量化执行的系统包括DuckDB、ClickHouse、Vectorwise、SQL Server Batch Mode、Snowflake/Velox 类执行框架等。3. JIT 是什么JIT 是在运行时为当前查询生成专门代码。例如这个条件WHERE amount 100 AND status PAID普通解释执行可能每一行都调用通用表达式解释器。JIT 会生成类似这样的专用函数bool filter(row) { return row.amount 100 row.status PAID; }然后编译成本机机器码执行。它的核心思想是不是优化“批量处理方式”而是减少解释器开销生成当前查询专用代码。4. 二者的关系它们不是互斥关系而是两种不同层次的优化。可以这样理解解释执行 一行一行用通用解释器执行 向量化执行 一批一批用通用向量算子执行 JIT 执行 为当前查询生成专用机器码执行 向量化 JIT 一批一批处理同时某些表达式/算子由 JIT 生成专用代码5. 核心区别对比项向量化执行JIT优化对象数据处理粒度代码执行方式核心思想一批数据一起处理为查询生成专用代码输入单位batch / vector / column chunk查询计划、表达式、IR、bytecode输出批量执行结果机器码或可执行函数是否需要编译通常不需要需要运行时编译启动开销较低有编译开销更适合OLAP、扫描、聚合、列式处理长查询、复杂表达式、CPU 密集型计算短查询表现通常也不错可能因编译成本不划算实现复杂度中到高高6. 用一个例子对比SQLSELECT sum(price * quantity) FROM orders WHERE status PAID;行式解释执行读一行 解释 status PAID 解释 price * quantity 累加 sum 读下一行特点简单但每行开销大。向量化执行读一批 status 批量生成过滤 mask 读一批 price、quantity 批量计算 price * quantity 批量累加 sum特点减少逐行开销适合列式和 OLAP。JIT 执行根据这个 SQL 生成专门的过滤和计算函数 编译成机器码 执行机器码特点表达式执行很快但前面要付编译成本。向量化 JIT每次处理一批数据 但过滤和表达式计算由 JIT 生成的专用代码完成这是两者结合的形式。7. 谁更好没有绝对更好取决于场景。向量化执行通常更稳。它没有明显的编译开销适合大量扫描、过滤、聚合、连接等分析型任务。JIT 在复杂表达式、长查询、重复执行的 query shape 上可能更强。但短查询可能还没跑多久JIT 编译成本就已经超过收益了。所以很多现代数据库会选择短查询解释执行或向量化执行 中大型 OLAP 查询向量化执行 复杂 CPU-bound 查询JIT 高性能系统向量化 JIT / codegen8. 一个直观类比向量化执行像工厂流水线一次处理一批零件效率来自批量化和缓存友好。JIT像为某个订单定制一台专用机器机器造出来之后跑得很快但造机器本身有成本。结论向量化执行和 JIT 是并列但可组合的数据库执行优化技术。更准确地说向量化执行减少“逐行处理”的开销 JIT减少“通用解释器”的开销它们解决的问题不同向量化关注数据怎么喂给 CPU JIT 关注CPU 执行什么代码现代高性能数据库常常会在二者之间做权衡甚至同时使用。回到顶部向量化执行与JIT的取舍可以把它们看成两条优化路线向量化执行一次处理一批数据靠 batch、列式内存、cache/SIMD 提升吞吐。JIT / Codegen为当前查询生成专用代码减少解释器、虚函数、分支和类型分派开销。一、明确同时使用向量化和 JIT / Codegen 的数据库系统是否同时使用说明ClickHouse是ClickHouse 是典型列式、向量化执行系统其官方文章说明 ClickHouse 也有 JIT可编译表达式、聚合函数、ORDER BY比较器等并且会用阈值和 LRU cache 避免重复编译成本。(ClickHouse)QuestDB是QuestDB 文档直接写明 Query Engine 包含JIT compiler和vectorized execution engine其 JIT 面向 full scan / partition scan 中的过滤表达式可生成机器码并可使用 SIMD/vector 指令。(QuestDB)SingleStore是偏 codegen columnar batchSingleStore 文档说明它使用 code generation把查询形状编译为高效机器码VLDB 论文也说明 SingleStore 使用 LLVM full-query code generation列存扫描会产出 column-oriented batch列存可利用 SIMD。(SingleStore 文档)Apache Spark SQL广义上是但不完全等同于 ClickHouse 这类 native vectorized engineSpark SQL 官方说它包含 cost-based optimizer、columnar storage 和 code generation但标准 Spark SQL 的 Whole-stage codegen 更多是 JVM 代码生成路线而 Databricks Photon 论文把 Spark SQL 归为 code-generated design并说明 Photon 选择了 vectorized-interpreted 路线。(Apache Spark)其中最典型的“向量化 JIT 混合”例子我会优先举ClickHouse、QuestDB、SingleStore。二、明确在向量化和 JIT 之间做取舍的数据库 / 引擎系统取舍方向为什么DuckDB选择向量化解释执行放弃 SQL JITDuckDB 论文明确说它使用 vectorized interpreted execution engine并且选择这种方式而不是 SQL JIT原因包括可移植性以及不想依赖 LLVM 这类大型编译器库。(DuckDB)Databricks Photon选择 native C 向量化解释执行而不是 code generationPhoton 论文明确写到它选择 vectorized-interpreted model in lieu of code generation原因包括运行时自适应、工程上更容易开发/调试/线上运维。论文也承认某些复杂条件表达式下 code generation 可能更快。(EECS Berkeley)Snowflake公开论文中偏向 columnar vectorized push-based 执行Snowflake 论文把其执行引擎描述为 columnar、vectorized、push-based数据以几千行为一批、列式格式流水线处理。它不是公开文献里常见的 HyPer/Impala 那种“主打全查询 JIT/codegen”的代表。Apache Doris偏向向量化Doris 文档说每个查询都运行在 vectorized engine 上Block 最多 4096 行这类设计主要押注 batch/column/SIMD而不是把通用 SQL JIT 作为核心路线。(Doris)PostgreSQL偏传统 iterator 可选 LLVM JITPostgreSQL 官方文档说明其 JIT 主要加速表达式求值和 tuple deforming它不是 ClickHouse/DuckDB 那种以向量化 OLAP 执行为核心的系统。(PostgreSQL)Impala / HyPer / Spark SQL偏 code generation / compilation 路线Photon 论文把 Spark SQL、HyPer、Apache Impala 归到 code-generated design 一侧而把 MonetDB/X100 这类归到 interpreted vectorized design 一侧。(EECS Berkeley)三、为什么有些系统两者都用有些只选一种向量化执行的优势是稳定、启动成本低、容易利用列式内存和 SIMD适合 OLAP 扫描、过滤、聚合、join。缺点是表达式逻辑很复杂时仍然会有一些解释/分派开销。JIT 的优势是能把当前查询的表达式、聚合、比较器、过滤条件融合成专用代码长查询、复杂表达式、重复 query shape 会受益。缺点是有编译开销、工程复杂、调试困难、内存占用和缓存管理问题。所以不同数据库的选择大概是DuckDB / Photon / Snowflake / Doris 更偏向向量化执行 PostgreSQL 更偏向传统 iterator 可选 JIT Impala / HyPer / Spark SQL 更偏向code generation / JIT 路线 ClickHouse / QuestDB / SingleStore 更像向量化 JIT/codegen 混合四、一个判断标准看到一个数据库时可以这样判断如果它说batch、chunk、block、vector、columnar batch、SIMD通常是向量化路线。如果它说LLVM、native code、machine code、code generation、query compilation、compiled expression通常是 JIT/codegen 路线。如果两个都出现而且是在执行引擎层面结合比如“columnar batches compiled expressions/filters/aggregates”那就是混合路线。ClickHouse、QuestDB、SingleStore 都属于这个方向。回到顶部Tiered JIT 是什么Tiered JIT可以翻译成分层即时编译 / 多级 JIT。它的核心思想是不要一上来就花很多时间生成最高质量机器码而是先用便宜方式跑起来如果代码真的变热再逐级编译成更快、更优化的机器码。1. Tiered JIT 是什么典型执行路径是Level 0解释执行 ↓ 代码执行次数变多收集 profile Level 1快速 JIT / baseline JIT编译快但优化少 ↓ 继续变热profile 更充分 Level 2优化 JIT编译慢但生成代码质量更高以 HotSpot JVM 为例官方文档说它有 client compiler 和 server compilerclient compiler 编译快、适合启动阶段server compiler 编译更慢、占用更多内存但生成更优化的机器码。Tiered compilation 就是把解释器、快速编译器和优化编译器组合起来。(Oracle 文档)Red Hat 对 HotSpot 的描述更直观Java 代码先在解释器中执行方法变 warm 后进入 quick compiler如果继续变 hot再进入 optimizing compiler执行可以先切到低层编译代码等更快的高层代码准备好后再切过去。(Red Hat Developer)2. 和普通 JIT 的区别普通 JIT 可以是解释执行一段时间 → 直接编译成一种机器码Tiered JIT 是解释执行 → 快速编译先获得中等性能 → 后台继续收集 profile → 对真正热点代码做重优化编译 → 必要时 deopt 回退所以它优化的是“启动延迟、编译成本、峰值性能”之间的平衡。对比普通 JITTiered JIT编译层级通常单层多层启动速度可能较慢尤其直接重优化更快先 baseline峰值性能取决于编译器热代码可进入优化层是否需要 profile可以有非常依赖 profile实现复杂度较低更高典型系统简单 VM、部分数据库 JITJVM、V8、.NET 等运行时.NET 官方文档也把 tiered compilation 描述为两层第一层快速生成代码或加载预编译代码第二层在后台生成优化代码。(Microsoft Learn)3. 数据库有没有用 Tiered JIT答案要分两类。4. 第一类数据库自己实现“SQL 层面的 Tiered JIT”生产数据库里比较少见。很多数据库有 JIT但不是典型 tiered JIT。例如 PostgreSQL 有 LLVM JIT但它主要是在计划阶段根据成本决定是否 JIT、是否 inline、是否做 expensive optimization这些决策发生在 plan time而不是运行中先 baseline、再优化、再动态切换。(PostgreSQL)PostgreSQL 的 JIT 主要加速表达式求值和 tuple deforming也就是为WHERE、投影、聚合表达式、tuple 解包生成专用代码。(PostgreSQL) 这更像cost-gated JIT不是经典的tiered JIT。QuestDB 也有 SQL JIT它会对 full scan / partition scan 中的过滤表达式生成机器码并且可能使用 SIMD/vector 指令文档没有把它描述为多层 tiered JIT。(QuestDB)所以PostgreSQL / QuestDB / ClickHouse 这类 有 JIT 或 codegen 但通常不是经典 HotSpot/V8 那种 tiered JIT5. 第二类运行在 JVM 上的 SQL 引擎间接使用 Tiered JIT这类可以说用了 tiered JIT但主要是在 JVM 层不一定是数据库自己实现的 SQL tiered JIT。典型例子Spark SQLSpark SQL 有 whole-stage code generation会把多个物理算子融合成一个 Java 函数并用 Janino 在运行时编译 Java 源码为 Java class。(japila-books)之后这些生成出来的 Java 方法会被 JVM 执行而 HotSpot JVM 默认支持 tiered compilation。Oracle 文档说明 tiered compilation 是 server VM 的默认模式。(Oracle 文档)所以 Spark SQL 的链路大致是SQL plan → Spark whole-stage codegen 生成 Java 代码 → Janino 编译成 JVM bytecode → JVM 解释 / C1 / C2 tiered JIT → 机器码这里的 tiered JIT 是JVM 提供的不是 Spark 自己写了一个 SQL tiered JIT。Trino / Presto 类 JVM SQL 引擎Trino 是 JVM 上的 SQL 引擎生产环境中会遇到 JVM JIT recompilation、code cache 等问题Trino 项目讨论中也提到它通过 JVM JIT 把 JVM bytecode 转成本机 CPU 指令。(trino.io)所以 Trino / Presto 这类系统也可以说查询执行代码 / 表达式代码 → JVM bytecode 或 Java 方法 → HotSpot tiered JIT但同样要注意这是依赖 JVM 的 tiered JIT不是数据库引擎自己管理 SQL 查询的多层 JIT。Calcite 系生态Apache Calcite 文档说明它使用 Janino 生成 Java 代码。(Apache Calcite) 因此基于 Calcite 并实际在 JVM 中执行生成代码的系统也可能间接受益于 JVM tiered JIT。6. 研究型系统里有更接近“数据库 Tiered JIT”的设计学术界有一些更接近数据库版 tiered JIT / adaptive compilation 的设计。比如Adaptive Execution of Compiled Queries这篇基于 HyPer 思路的论文提出了一个框架先用专门为数据库查询设计的 bytecode interpreter 执行同时动态跟踪查询进度并在解释执行和编译执行之间切换以兼顾短查询低延迟和长查询高吞吐。这非常接近数据库里的 tiered JIT 思路短查询先解释执行避免编译成本 长查询后台编译完成后切换到机器码图数据库方向也有 adaptive query compilation 研究引擎先解释执行同时检索或生成编译代码用来隐藏编译或代码加载开销。(Springer Nature Link)7. 总结Tiered JIT 分阶段 JIT先快编译后重优化。放到数据库里可以这样理解不是 tiered JIT SQL → 计划 → 要么解释执行要么一次性 JIT 成机器码 tiered JIT SQL → 计划 → 先解释 / bytecode / baseline code 执行 → 查询或表达式变热 → 后台生成更优化机器码 → 动态切换过去目前结论是类型例子是否算 Tiered JITJVM SQL 引擎Spark SQL、Trino / Presto、Calcite 系间接是依赖 JVM tiered JITPostgreSQL LLVM JITPostgreSQL不是典型 tiered JIT更像成本阈值控制的 JIT原生 SQL JITQuestDB、ClickHouse 等有 JIT/codegen但通常不叫 tiered JIT研究型 adaptive query compilationHyPer 相关论文、图数据库 adaptive compilation很接近数据库版 tiered JIT