⚡ 现已推出具有无限 IOPS 的极速 NVMe 驱动器。了解 PlanetScale Metal

博客|工程

目录

目录

想了解更多关于 Metal 的无限 IOPS、Vitess、水平分片或企业级选项的信息吗? 与解决方案团队交谈 获取 RSS 订阅

更快的 Go 解释器:追赶 C++ 的脚步

作者:Vicent Martí | 2025 年 3 月 20 日

Vitess(为 PlanetScale 提供支持的开源数据库)附带的 SQL evaluation engine 最初是作为 AST evaluator 实现的,该 evaluator 直接操作由我们的解析器生成的 SQL AST。 在过去的一年中,我们逐渐用 Virtual Machine 替换了它,尽管它是用 Go 原生编写的,但其性能与 MySQL 中原始的 C++ evaluation code 相似。 最值得注意的是,新的 Virtual Machine 一再证明自己比原来的 Go 解释器更容易维护,即使它的速度快了几个数量级。 让我们回顾一下我们为获得这些令人惊讶的结果所做的实现选择。

什么是 SQL evaluation engine?

Vitess 旨在实现无限的水平扩展。 为此,所有对 Vitess 集群的查询都必须通过一个 vtgate。 由于您可以根据需要部署任意数量的 vtgate 实例(因为它们本质上是无状态的),因此您可以线性地增加集群的容量。 每个 gate 的工作是整个分布式系统中最复杂的部分。 它解析传入查询的 SQL,并创建一个 shard-aware query plan,我们在集群的一个或多个 shard 中对其进行 evaluation。 然后,我们聚合这些 evaluations 的结果,并将它们返回给用户。

Vitess 在实践中运行良好(在性能和易于采用方面)的原因之一是集群中的每个 shard 都由一个真实的 MySQL 实例支持。 即使是更复杂的 SQL 查询也可以分解为更简单的语句,这些语句在底层 MySQL 数据库中进行 evaluation。 因此,这些查询的结果始终与您直接查询 MySQL 时的预期结果相匹配。

然而,现实世界中的 SQL 查询可能会变得_非常复杂_。 我们需要支持普通 MySQL 实例支持的几乎所有类型的查询,但我们需要跨多个 MySQL 实例对其进行 evaluation。 这意味着有时,我们无法回退到 MySQL 来 evaluate 我们所有的 SQL 表达式。

考虑一个相当简单的查询,例如:

SELECT inventory.item_id, SUM(inventory.count), AVG(inventory.price) AS avg_price
FROM inventory
WHERE inventory.state = 'available' AND inventory.warehouse IN ? 
GROUP BY inventory.item_id
HAVING avg_price > 100;

假设此查询在分片的 Vitess 集群中执行,则库存项目可能存在于任何 shard 中。 因此,我们的查询规划器将准备一个计划,该计划并行查询所有 shard,将部分聚合推送到 MySQL,然后我们将在 vtgate 中本地执行聚合 (SUMAVG)。 WHERE 子句中的 statewarehouse 检查可以并且将直接在为每个 shard 提供支持的 MySQL 实例上执行。 但是最后一个表达式 avg_price > 100 适用于聚合的结果,该结果仅在 Vitess 中可用。 这就是 Vitess evaluation engine 发挥作用的地方。

我们的 evaluation engine 是一个解释器,它支持 MySQL 使用的 SQL 方言中的大多数标量表达式。 这不包括诸如执行 JOINGROUP BY 的 grouping 等高级构造(这些直接由规划器执行,正如我们所看到的),而是您将看到的作为 WHERE 子句或 GROUP BY 子句的条件的实际子表达式。 任何无法降低到由规划器在 MySQL 中执行的 SQL 片段都由引擎在 Go 中本地 evaluate。

当然,这些 SQL 子表达式并非任意复杂。 它们甚至不是图灵完备的(因为它们不能循环!),因此您可能认为像 avg_price > ? 这样的语句的 evaluation 应该很简单,但与大多数工程问题一样,在现实世界中执行这些操作时存在大量的细微差别。

SQL 是一种极其动态的语言,充满了怪癖,而 MySQL 中的 SQL 更是如此。 我们花费了大量的时间来使 SQL evaluation 的每个角落案例都与 MySQL 的行为完全匹配。 事实上,我们的 测试 套件fuzzer 是如此全面,以至于我们经常在原始 MySQL evaluation engine 中发现错误,我们必须在 upstream 中修复这些错误(比如这个 collation 错误insert SQL 函数中的这个错误 或 [搜索子字符串时的这个错误](https://planetscale.com/blog/https:/github.com/mysql/mysql-server/pull/515))。 尽管如此,完全准确是不够的。 对于大多数查询,这些表达式对于每个返回的行都要 evaluation 一次甚至多次,因此为了不引入额外的开销,evaluation 需要尽可能快。

如前所述,Vitess 中 evaluation engine 的第一个版本是一个基于 AST 的解释器,直接在我们的解析器生成的 SQL AST 之上运行。 这是一个非常简单的设计,使我们能够专注于_准确性_,但牺牲了性能。 让我们讨论一下用于用功能完善的 virtual machine 替换此解释器的新设计,该 virtual machine 既更快又更易于维护。 从基础开始。

解释器的形态

对于那些刚接触编程语言实现的人来说,大致有 3 种方法可以在运行时执行_动态_语言。 复杂性和性能递增:

  1. 基于 AST 的解释器,其中将语言的语法解析为 AST,并通过递归地遍历 AST 的每个节点并计算结果来执行 evaluation。 (这是 Vitess 中 evalengine 过去的工作方式!)
  2. bytecode VM,其中将 AST 编译为二进制 bytecode,该 bytecode 可以由 virtual machine 进行 evaluation — 一段模拟 CPU 的代码,但具有更高级别的指令。 (这是我们最近发布的!)
  3. JIT 编译器,其中将 bytecode 直接编译为主机平台的本机指令,因此它可以直接由 CPU 执行,而无需 virtual machine 的解释。 (我们稍后会讨论这个!)

这里要考虑的第一件事是从性能的角度来看,从 AST 解释器升级到 virtual machine 是否有意义。 这是一个直觉:SQL 表达式非常动态(在类型方面)、非常高级(在每个原始操作方面),并且具有非常少的控制流(在 evaluation 方面 - SQL 表达式实际上不会循环,并且条件很少见;它们的流程始终是线性的!)。 这可能会让我们相信,从将基于 AST 的 evaluation engine 转换为 bytecode 无法提高性能。 AST 已经非常适合高级操作和类型切换!

这只是_表面上_正确。 许多编程语言都是高度动态的,并且它们比使用 AST 解释器更有效地在 bytecode VM 中运行。 一个明显的例子是 Ruby 从其在 MRI 中的原始 AST 解释器到 YARV(bytecode VM)的古老过渡。 Python 也很早就做了类似的切换。 您可以肯定,实际上没有 JavaScript 引擎在使用 AST evaluation:即使这些引擎的目标是尽快开始运行 JS,它们仍然会在 JIT 编译开始之前编译为(非常高效的)bytecode 解释器。

那么,virtual machine 与 AST 解释器相比有什么优势? 其中很大一部分归结为指令调度,可以使其非常快速(稍后会详细介绍!)。 但确实,对于 SQL 表达式,我们实际上将执行很少的指令。 因此,为了从 VM 中获得性能,我们将不得不提出新的技巧。

我最初为我们的 SQL virtual machine 设想的方法是基于 Stefan Brunthaler 的 使用 Quickening 的高效解释。 这篇论文背后的想法是,由于缺少关于类型的信息,动态编程语言很难有效地执行。 诸如 a + 1 之类的简单表达式必须以完全不同的方式进行解释,具体取决于 a 是整数、浮点数还是字符串。 为了在实践中优化这些操作,该论文建议将 bytecode 从更通用的指令(例如,需要确定两个操作数的类型以了解如何将它们相加的 sum 运算符)重写为特定的静态指令,这些指令专门用于在运行时操作它们的类型(例如,知道两个操作数都是整数并可以立即将它们相加的 sum 运算符)。

为此,quickening VM 需要在_运行时_确定正在 evaluation 的表达式的类型,并逐步将 bytecode 重写为对其进行操作的指令。 这在实践中很难做到! 但是在为 SQL 中不同类型的运算符实现了一大堆专门的指令并尝试在运行时重写它们之后,我注意到有机会通过使其更有效且至关重要的是更简单来进一步推进这个想法。

事实证明,我们在 Vitess 中执行的语义分析足够高级,通过与 upstream MySQL 服务器及其信息模式的仔细集成,它可以用于_静态类型化我们正在执行的 SQL 表达式的 AST_。 这花费了大量的精力来实现,但带来了巨大的胜利:由于规划器知道将用于 evaluate 每个 SQL 表达式的实际输入的类型,我们可以从这些类型推导出编译时所有子表达式的类型,从而生成已经专门化的 bytecode,而无需运行时重写。

现在我们只需要实现一个 Virtual Machine 来有效地解释专门化的 bytecode!

Go 中高效的 Virtual Machine

实现 VM 通常涉及很多复杂性。 正如我们所解释的,您必须编写一个编译器来处理输入表达式 AST 并生成相应的二进制指令(您甚至必须提出一种编码!),并且_之后_您必须实现实际的 VM,该 VM 解码每个指令并执行相应的操作。 而且您必须始终保持它们同步! 发出 bytecode 的编译器和执行它的 VM 之间的任何不匹配通常是灾难性的并且很难调试。

从历史上看,bytecode VM 一直以相同的方式实现:一个巨大的 switch 语句。 您解码一个指令,并根据其类型进行 switch 以跳转到需要执行的操作。 这通常是对抗 AST 解释器的性能优势,因为在实践中 switching 非常快(尤其是在像大多数 VM 一样用 C 或 C++ 实现时),并且允许线性地执行,而无需递归。

然而,这种设计也有其自身的缺点。 JIT 大师和 LuaJIT 的作者 Mike Pall 在 2011 年的这篇邮件列表文章 中非常深刻地阐述了这些问题。 让我为这篇博客总结一下:除了 VM 的指令需要与编译器保持同步之外,在具有许多指令的语言中,主 VM 循环的实际性能在实践中并不理想,因为编译器通常在编译大型函数时会遇到困难,而这些函数_确实_很大。 它们在 switch 的每个分支上将寄存器溢出到各处,因为很难判断哪些分支是热的,哪些分支是冷的。 随着所有的 pushing 和 popping,跳转到 switch 的分支通常看起来更像是一个函数调用,因此 virtual machine 的许多性能优势消失了。

Mike 在那篇文章中讨论的是 C 编译器,但可以肯定的是,对于用 Go 实现的 virtual machine 来说,这些问题是相同的。 经过大量的测试,我可以向您保证,它们实际上_更糟_,因为 Go 编译器在优化方面不是很出色。 在优化和快速编译时间之间总是需要权衡,而 Go 作者历来选择后者。

Go 的一个关键问题是,通常 switch 语句的不同分支通过_二进制搜索_而不是跳转表跳转到。 Switch 跳转表优化是 在编译器上出人意料地迟实现 的,并且在实践中它_非常棘手_,没有任何方法可以强制执行它。 您必须仔细调整 VM 指令的编码方式,以确保您在 VM 的主循环中跳转,并且除了自己查看生成的汇编之外,您无法可靠地检查 virtual machine 的调度代码是否已正确优化。

显然,基于 switch 的 VM 循环不是编写高效解释器的最先进的方法,无论是在 Go 还是在任何其他编程语言中。 那么什么是最先进的技术呢? 好吧,当涉及到 Go 时,事实证明现在没有人做快速解释器(至少我找不到)。 那些在这里做有趣工作的人,例如 wazero WASM 实现,正将他们的性能精力集中在 JIT 上。 因此,我们将不得不进行创新!

在 Go 之外,用 C 或 C++ 实现的解释器最有趣的方法是 continuation-style evaluation loops,正如 2021 年的这份报告中所见,该报告为解析 Protocol Buffers 实现了该技术。 这涉及将 VM 的所有 opcode 实现为在 VM 上作为参数运行的独立函数,并且函数的返回值是_对计算的下一步的回调_。 这听起来像是一些昂贵且递归的东西,但诀窍是新版本的 LLVM 允许我们将函数标记为_强制性_尾调用(参见:https://en.wikipedia.org/wiki/Tail_call),因此生成的代码不是递归地调用 VM 循环,而是_跳转_在操作之间,并使用独立函数作为抽象来控制寄存器放置和溢出。 最新发布的 Python 3.14 实际上 附带了基于这种设计的解释器,在执行 Python 代码时,性能提高了 30%。

不幸的是,这不是我们可以在 Go 中做的事情,因为正如我们前面讨论的那样,Go 编译器对优化过敏。 它_有时_可以发出尾调用,但它需要以正确的方式进行调整,并且除非在编译时保证尾调用,否则此实现根本无法在实践中工作。 但是,如果我们对每个指令都保持与独立函数相同的设计,并且不进行尾调用,而是在每个指令之后强制将控制权返回给 evaluation 循环,该怎么办? 这可以通过不将我们的编译程序作为“byte code”发出,而是发出 指向每个指令的函数指针的切片 来非常容易地实现。 该设计可能有点违反直觉,但它具有许多非常有趣的属性。

首先,VM 变得微不足道! 它只是几行代码,并且不必担心优化任何大型 switch 语句。 它只是重复地一个接一个地调用函数! 这是一个简化的示例,但是如果您查看 Vitess 中的实际实现,您会发现真正的 virtual machine 实现几乎不比这复杂。

func (vm *VirtualMachine) execute(p *Program) (eval, error) {
	code := p.code
	ip := 0
	for ip < len(code) {
		ip += code[ip](vm)
		if vm.err != nil {
			return nil, vm.err
		}
	}
	if vm.sp == 0 {
		return nil, nil
	}
	return vm.stack[vm.sp-1], nil
}

当执行每个指令时,我们需要返回的只是指令指针 ip 的偏移量。 大多数函数返回 1,这会导致执行下一个指令,但是通过返回负值或正值,您可以实现所有控制流,包括循环和条件。

除了大大简化的 virtual machine 之外,这种方法的第二个优点是编译器_也_变得微不足道,因为没有 bytecode! 相反,编译器通过将“回调”推送到切片中来直接发出各个指令。 没有要跟踪的指令 opcode,没有要执行的编码,也没有要与 VM 保持同步的内容。 开发编译器意味着同时开发 VM,这极大地提高了迭代速度,并防止了在开发 virtual machine 时经常发生的一整类错误。

func (c *compiler) emitPushNull() {
	c.emit(func(vm *VirtualMachine) int {
		vm.stack[vm.sp] = nil
		vm.sp++
		return 1
	})
}

您可能会注意到,在为非平凡的语言建模指令时,这里出现了一个小问题:如果没有指令编码,那么我们就不能有带参数的指令。

这在像 C 这样的语言(传统上用于实现大多数编程语言解释器)中是一个大问题,这就是为什么这种技术在那里永远不会出现。 但实际上这对我们来说不是问题,因为 Go 编译器实际上支持_闭包_! 我们可以发出我们想要的任何指令,并且 Go 编译器会自动将它的参数捕获在回调中。 我们不必考虑如何在 bytecode 中编码我们的参数,事实上,我们的参数可以像它们需要的那样复杂:生成的回调将包含 Go 编译器创建的它们的副本。 它本质上是一个可怜的人的 JIT,由编译器辅助,并且它在实践中工作得非常好,无论是在性能方面还是在人体工程学方面。

查看此编译器方法,该方法生成一个指令,以将来自输入行的 TEXT SQL 对象推送到堆栈中:

func (c *compiler) emitPushColumn_text(offset int, col collations.TypedCollation) {
	c.emit(func(vm *VirtualMachine) int {
		vm.stack[vm.sp] = newEvalText(vm.row[offset].Raw(), col)
		vm.sp++
		return 1
	})
}

输入 rows 数组中的偏移量_和_文本的 collation 都静态地烘焙到生成的指令中!

几乎是静态类型

使用 SQL 表达式的完全静态类型(从规划器中的类型信息派生),我们可以设计一个极其高效的 virtual machine,其中每个指令都专门用于它在其上执行的操作数的类型。 这是 VM 的最佳和最简单的设计,因为我们从不需要在 evaluation 期间进行类型切换。 但是我们正在这里处理 SQL(或者,更准确地说,MySQL 的 SQL 方言),所以并非一切都是彩虹和独角兽。 很多时候情况恰恰相反。

让我们考虑这个非常复杂的 SQL 表达式:-inventory.price。 也就是说,查询的 inventory.price 列中每个值的取反。 我们知道(感谢我们的语义分析和模式跟踪器)inventory.price 列的类型是 BIGINT。 那么 -inventory.price 的类型是什么? 没有 SQL 神奇世界经验的幼稚读者可能会认为结果类型是 BIGINT,但实际情况并非如此!

在绝大多数情况下,BIGINT 的取反确实会产生另一个 BIGINT 值。 但是当 BIGINT 的实际值为 -9223372036854775808(即可以在 64 位中表示的最小值)时,对其取反会将该值提升为 DECIMAL,而不是静默地截断它或返回错误。 您可以看到这如何轻松地在我们的 virtual machine 的静态编译指令中抛出一个扳手。 突然,我们计算的静态类型检查不再有效,因为表达式的类型不再取决于输入的类型,而是取决于输入的实际_值_。 为了继续 evaluate 此取反的结果,我们总是必须在运行时再次进行类型检查,从而从一开始就破坏了静态类型的全部意义。

为了解决这个问题,我们_没有_在运行时引入更多的类型切换。 我们正在使用一个经典技巧,这种技巧可以在 JIT 编译代码中随处可见,但在 virtual machine 中却很少见,甚至从未见过:de-optimization。 有一个小的表达式列表,其中极端情况(例如,溢出)可能导致运行时动态类型化。 每当发生这种情况时,我们只需退出在我们的 virtual machine 中执行的操作,然后退回到在旧的 AST evaluator 上执行,该 evaluator 一直在运行时执行类型切换。 这与 JIT 编译器在检测到值的运行时类型不再与它们发出的生成代码匹配时所做的事情非常相似; 它们从本机代码回退到 virtual machine。 在我们的例子中,我们落后一步,从 virtual machine 回退到 AST 解释器,但是性能含义是相同的。 这种设计使我们能够保持我们的解释器执行静态类型的代码,而无需在运行时进行任何类型切换。 这是一个编译后的整数取反的示例:

func (c *compiler) emitNeg_i() {
	c.emit(func(vm *VirtualMachine) int {
		arg := vm.stack[env.vm.sp-1].(*evalInt64)
		if arg.i == math.MinInt64 {
			vm.err = errDeoptimize
		} else {
			arg.i = -arg.i
		}
		return 1
	})
}

然而,这种方法存在一个明显的缺点:AST 解释器的代码永远无法从 Vitess 中删除。 但总的来说,这不是一件坏事。 就像大多数高级语言运行时尽管有 JIT 编译器也保留了它们的 virtual machine 解释器一样,访问我们的经典 AST 解释器给了我们多功能性。 当我们检测到表达式只会被 evaluation_一次_时,可以使用它(例如,当我们使用 evaluation engine 对 SQL 表达式执行常量折叠时)。 在这些情况下,编译然后在 VM 上执行的开销超过了在 AST 上进行单次 evaluation 的开销。 最后,在准确性方面,能够相互 fuzz AST 解释器和 VM 已经成为检测错误和极端情况的宝贵工具。

结论

这种 virtual machine 实现技术并非完全新颖(我以前在野外看到它用于基于规则的授权引擎!),但据我所知,它从未在 Go 中使用过。 鉴于语言和编译器的限制,该技术产生了惊人的结果:Vitess 中的新 SQL 解释器更快。 编写更快、维护更快、执行更快。 这些基准测试说明了一切:

Vitess 中 Evalengine 的性能随时间的变化

Benchmark results

在这里,我们对三个实现之间 5 个不同查询(从非常复杂到非常简单)的性能进行了比较:

  1. old,它是 evalengine 的原始基于 AST 的动态实现。
  2. ast,它是将静态类型检查添加到 virtual machine 并使用它们来部分优化 AST evaluator 的结果。
  3. vm,它是基于回调的 virtual machine 实现,如本文中所讨论的。

最近的结果与 MySQL 的比较

Benchmark results With MySQL

这是我们的 evaluation engine 与 MySQL 中的本机 C++ 实现相比的当前性能。 请注意,测量 MySQL 花费在 evaluation 上的时间非常棘手; 这些不是查询的总响应时间,而是 mysqld 服务器中手动 instrumentation 的结果,以确保公平的比较。

原始基准测试数据

                   │   ast   │         vm         │         mysql          │
                   │  sec/op  │  sec/op   vs base        │  sec/op   vs base          │
CompilerExpressions/complex_arith-32  162.75n ± 1%  50.77n ± 1% -68.81% (p=0.000 n=10)  49.40n ± 5% -69.64% (p=0.000 n=10+184)
CompilerExpressions/comparison_i64-32  30.30n ± 2%  16.95n ± 1% -44.08% (p=0.000 n=10)  26.93n ± 22% -11.12% (p=0.000 n=10+11)
CompilerExpressions/comparison_u64-32  30.57n ± 3%  17.49n ± 1% -42.78% (p=0.000 n=10)  18.80n ± 9% -38.53% (p=0.000 n=10+16)
CompilerExpressions/comparison_dec-32  70.75n ± 1%  52.58n ± 2% -25.68% (p=0.000 n=10)  46.59n ± 5% -34.14% (p=0.000 n=10+14)
CompilerExpressions/comparison_f-32   53.05n ± 1%  25.65n ± 1% -51.64% (p=0.000 n=10)  27.75n ± 23% -47.69% (p=0.000 n=10)
geomean                 56.30n    28.94n    -48.60%         31.76n    -43.58%
                   │  ast   │          vm          │
                   │  B/op  │  B/op   vs base           │
CompilerExpressions/complex_arith-32  96.00 ± 0%  0.00 ± 0% -100.00% (p=0.000 n=10)
CompilerExpressions/comparison_i64-32  16.00 ± 0%  0.00 ± 0% -100.00% (p=0.000 n=10)
CompilerExpressions/comparison_u64-32  16.00 ± 0%  0.00 ± 0% -100.00% (p=0.000 n=10)
CompilerExpressions/comparison_dec-32  64.00 ± 0%  40.00 ± 0%  -37.50% (p=0.000 n=10)
CompilerExpressions/comparison_f-32   16.00 ± 0%  0.00 ± 0% -100.00% (p=0.000 n=10)
                   │  ast   │          vm          │
                   │ allocs/op │ allocs/op  vs base           │
CompilerExpressions/complex_arith-32  9.000 ± 0%  0.000 ± 0% -100.00% (p=0.000 n=10)
CompilerExpressions/comparison_i64-32  1.000 ± 0%  0.000 ± 0% -100.00% (p=0.000 n=10)
CompilerExpressions/comparison_u64-32  1.000 ± 0%  0.000 ± 0% -100.00% (p=0.000 n=10)
CompilerExpressions/comparison_dec-32  3.000 ± 0%  2.000 ± 0%  -33.33% (p=0.000 n=10)
CompilerExpressions/comparison_f-32   2.000 ± 0%  0.000 ± 0% -100.00% (p=0.000 n=10)

结果是惊人的:在新 VM 中运行的预编译 SQL 表达式比 Vitess 中 SQL evaluation 的第一个实现快 20 倍,并且在大多数情况下,我们已经赶上了 MySQL 中 C++ 实现的性能。 未在图表中显示但可以在原始基准测试数据中看到的另一个细节是,新的 virtual machine 不分配内存来执行 evaluation — 这是由于静态类型检查而产生的完全专门化的指令的一个非常好的副作用。

总的来说,我们将获得与 MySQL 的 C++ evaluation engine 相同的性能范围视为一项巨大的工程成功,尤其是在生成的实现如此易于维护的情况下。 Go 和 C++ 之间总是会存在性能差距,这是由于 Go 编译器中质量与编译速度之间的权衡以及语言本身的语义所致,但正如我们在此处所展示的,这种差距并非无法克服。 凭借专业知识和精心的设计,有可能获得开发和部署 Go 服务的许多好处,而无需支付该语言固有的性能损失。 在这种特定情况下,我们通过有能力执行语义分析和静态键入 SQL 表达式(MySQL 不会这样做)以及通过选择一种高效的 virtual machine 设计来实现,该设计利用了 Go 的优势,而不是对抗其局限性。

附录:那么为什么不用 JIT?

好奇的人可能想知道:下一步是什么? 我们接下来要做 JIT 编译吗? 答案是否定的。 虽然这种编译器和 VM 的设计看起来是实施完整的 JIT 编译器的绝佳起点_在理论上_,但在实践中,优化和复杂性之间的权衡是没有意义的。 JIT 编译器对于编程语言非常重要,因为它们的 bytecode 操作可以优化为非常低的抽象级别(例如,“add”运算符只需要执行本机 x64 ADD)。 在这些情况下,调度指令的开销变得如此重要,以至于用 JITted 代码块替换 VM 的循环会带来显着的性能差异。 然而,对于 SQL 表达式,即使在我们进行专门化之后,大多数操作仍然保持非常高的级别(例如“将此 JSON 对象与路径匹配”或“将两个固定宽度的十进制数加在一起”)。 正如我们的基准测试中所测量的,指令调度的开销小于 20%(并且可以在 VM 的循环中进一步优化)。 20% 不是在您开始使用原始汇编进行 JIT 之前要针对的目标数字。 因此,在这一点上,我的直觉是 JIT 编译将是一种不必要的复杂且无用的优化。