
最近,DolphinDB 对 JIT 编译器进行了新一轮重构,底层编译框架升级为 MLIR(Multi-Level Intermediate Representation)。MLIR 是一种用于构建编译器的通用中间表示框架,连接高层代码表示与底层机器码生成,为编译优化提供了更加灵活的基础。
这不仅仅是一次常规的版本更新。相比介绍新增了哪些语法、支持了哪些类型,我们更想回答另一个问题:
在已经拥有成熟向量化计算能力之后,为什么 DolphinDB 还要投入重构这样一套编译执行能力?
过去很多年,在数据计算领域几乎有一条默认原则:能向量化,就不要写 for。
无论是 NumPy、Pandas,还是 SQL、DolphinDB,这条原则几乎都成立。原因并不复杂:向量化能够将大量元素级计算交给底层高性能实现,在一次函数调用中完成整个数据批次的处理,从而避免解释器反复执行循环所带来的运行时开销。
因此,在大多数数据处理场景中,向量化始终是性能和开发效率兼顾的最佳选择。
但现实业务,并不总是如此。
越来越多企业计算开始包含复杂的控制流逻辑:策略回测需要根据上一笔成交不断更新持仓状态;实时风控需要维护账户状态并根据不同条件触发处理逻辑;CEP(复杂事件处理)需要随着事件流持续更新状态;金融定价、数值求解则需要不断迭代逼近结果……
这些计算有一个共同特点:当前计算依赖此前的中间状态,而不是简单地对每个元素执行相同的计算。
这意味着,将它们强行转换为向量化表达,往往会增加实现复杂度,甚至未必能够获得更好的性能收益。
那么,当向量化覆盖不到的时候,还有没有另一条性能优化路径?
很多人认为,循环慢,是因为 CPU 执行循环效率低。实际上,在很多解释型语言中,真正的开销往往并不来自循环本身,而是解释执行过程。
每一次循环迭代,都可能涉及变量解析、动态类型检查、函数调度、对象访问等运行时工作。当这些操作需要重复执行数百万次时,解释器带来的额外开销,甚至可能超过真正的数据计算。
因此,在对性能要求极高的场景中,一种常见做法,是将热点逻辑迁移到 C++ 等编译型语言实现。
这种方式确实能够获得更高性能,但同时也意味着新的成本:维护多语言工程、编译动态库、跨语言接口,以及随业务演进不断增加的维护复杂度。
有没有一种办法,既保留脚本开发效率,又能够获得接近原生代码的执行速度?
这正是 JIT(Just-In-Time,即时编译)试图解决的问题。
很多人第一次接触 JIT,会误以为它是一种新的计算优化算法。事实上,JIT 改变的并不是算法本身,而是代码的执行方式。
普通脚本运行时,代码通常由解释器逐句执行;而 JIT 会在程序运行过程中,将热点代码编译成本地机器码,并缓存编译结果,后续再次执行时便可以直接运行机器码,而无需重复经过解释执行。

因此,从开发者的角度来看,业务逻辑没有改变;真正变化的是背后的执行链路:这段代码不再依赖解释器逐句执行,而是以更接近原生程序的方式运行。
从 Java HotSpot、.NET CLR,到 V8、PyPy、Julia 等运行时,都采用了类似的思想,只是在热点识别、编译框架和优化策略上各有不同。
对于循环、分支、状态更新等解释执行开销占比较高的计算,JIT 往往能够显著降低运行时开销,因此成为许多高性能运行时的重要组成部分。
此前的 JIT 实现在应对复杂控制流和多层类型推导时,优化空间相对有限。这次重构选择 MLIR 作为底层编译框架,正是为了构建更灵活的编译优化基础。MLIR 提供了更丰富的中间表示和优化能力,使 DolphinDB 的 JIT 能够更好地支持类型推导和控制流优化,也为未来扩展更复杂的语法特性和更多数据类型提供了基础。
但需要说明的是 JIT 并不是用来替代向量化。对于聚合、筛选、窗口计算等数据分析任务,向量化始终是效率最高、代码最简洁的方案。我们并不希望开发者为了使用 JIT,而放弃原本更优雅、更高效的向量化表达。这次重构补齐的,是向量化覆盖不到的那部分计算,让那些无法向量化的复杂业务,同样能够获得接近原生代码的执行效率。
或者说,两者分别解决的是两类不同的问题:向量化解决的是数据并行,JIT 解决的是流程控制。当计算能够表示为“对整批数据执行相同操作”时,向量化通常是最佳选择;而当业务逻辑天然依赖循环、分支、状态更新等控制流时,仅靠向量化就不再适用,JIT 则提供了另一种高性能实现方式。
JIT 的使用方式非常简单:只需在函数或类定义前添加一个 @jit 注解即可。需要注意的是,当前这一版 JIT 对运行环境有一定要求,需要 server 满足对应的 ABI 版本才能使用,具体要求可参考官方文档说明。
@jit
def positiveSum(v) {
total = 0.0
for (x in v) {
if (x > 0) {
total += x
}
}
return total
} 注:这里只用于展示 @jit 的基本写法。对于这类简单过滤求和,实际业务中仍然建议优先使用向量化表达。
开发者几乎无需调整原有代码,就可以将热点函数交由新的 JIT 编译器处理。
并不是所有代码都适合开启 JIT。如果一个计算本来一句 sum() 就能完成,那么向量化通常仍然是最佳选择。真正值得使用 JIT 的,是那些解释执行成本远高于计算成本的热点逻辑,例如:
这些场景共同的特点是:很难改写成向量化表达,但又会被重复执行成千上万次。
下面用两个具体案例说明。本次测试基于 Ubuntu 20.04 系统(内核 5.15),CPU 为 i5-10400F(6 核 12 线程)。
隐含波动率通常不存在解析解,需要通过迭代逼近求得,例如采用二分法在给定区间内不断收缩范围,直至误差满足精度要求。每一步计算都依赖上一步收缩后的区间,这种路径依赖的逻辑难以用向量化表达,只能以循环方式实现。
@jit
def impliedVolatility(futurePrice, strikePrice, ttm, riskRate, carryRate, optionPrice, isCall) {
high = 5.0
low = 0.0
do {
mid = (high + low) / 2.0
if (blackScholes(futurePrice, strikePrice, ttm, riskRate, carryRate, mid, isCall) > optionPrice) {
high = mid
} else {
low = mid
}
} while ((high - low) > 0.00001)
return (high + low) / 2.0
}每次循环都需要重新计算一次期权定价(blackScholes),并根据结果收缩区间,直至收敛。测试规模为 10,000 份期权合约,重复调用 10 次,结果如下(正确性校验通过):

止损判断同样是典型的路径依赖计算:当前收益需要与历史最高点持续比较,一旦回撤幅度超过阈值即应立即返回,无需继续遍历后续数据。这种“边计算边判断、可能提前退出”的逻辑,同样难以用向量化方式表达。
@jit
def stopLossIndex(ret, threshold) {
currentReturn = 1.0
peakReturn = 1.0
i = 0
while (i < size(ret)) {
currentReturn *= 1.0 + ret[i]
if (currentReturn > peakReturn) {
peakReturn = currentReturn
}
drawdown = 1.0 - currentReturn / peakReturn
if (drawdown >= threshold) {
return i
}
i += 1
}
return -1
}在 100 万条收益率记录的测试数据下,重复调用 10 次,止损阈值 0.15(触发位置第 999,999 条),结果如下(正确性校验通过):

除了以上两个案例,在最基础的循环求和测试中,完成首次编译预热后,JIT 版本相比普通解释执行获得了超过 55 倍的性能提升。
当然,这并不意味着任何代码都会获得几十倍提升。JIT 的实际收益与代码结构、数据规模、参数类型稳定性等因素密切相关。因此,更合理的做法,是根据业务特点选择合适的实现方式,而不是将 JIT 作为所有场景的默认选择。
也有几类场景并不适合使用 JIT:
一是已经能通过向量化高效完成的计算。例如简单的聚合、筛选、加减乘除等,向量化已经足够高效,强行使用 JIT 不仅无法带来明显提升,反而可能因为编译开销而得不偿失。
二是仅执行一次或很少执行的冷代码。JIT 的优势在于“编译一次,执行多次”。如果一段代码只运行一次,编译开销甚至可能超过直接解释执行的耗时。
三是参数类型频繁变化的热点函数。JIT 会针对不同的参数类型分别生成和缓存机器码,频繁切换类型会导致重复编译,抵消 JIT 带来的收益。
判断是否适合使用 JIT,本质上是一个简单的权衡:编译开销 + 机器码执行时间 < 解释执行时间。只有当代码被执行足够多次,且执行时间远大于编译时间时,JIT 才能发挥真正的价值。
此次基于 MLIR 的 JIT 重构,并不是为了替代向量化,而是希望让更多无法自然向量化的复杂业务,同样能够获得高性能的执行能力,是对复杂控制流计算能力的重要补充。向量化负责数据并行,JIT 负责流程控制,两者相辅相成,共同构成了 DolphinDB 更加健全、更具扩展性的脚本执行引擎。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。