首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >性能再进阶:DolphinDB JIT 编译器完成重构升级

性能再进阶:DolphinDB JIT 编译器完成重构升级

原创
作者头像
DolphinDB
发布2026-07-23 16:22:16
发布2026-07-23 16:22:16
1460
举报

最近,DolphinDB 对 JIT 编译器进行了新一轮重构,底层编译框架升级为 MLIR(Multi-Level Intermediate Representation)。MLIR 是一种用于构建编译器的通用中间表示框架,连接高层代码表示与底层机器码生成,为编译优化提供了更加灵活的基础。

这不仅仅是一次常规的版本更新。相比介绍新增了哪些语法、支持了哪些类型,我们更想回答另一个问题:

在已经拥有成熟向量化计算能力之后,为什么 DolphinDB 还要投入重构这样一套编译执行能力?

向量化不是万能的

过去很多年,在数据计算领域几乎有一条默认原则:能向量化,就不要写 for。

无论是 NumPy、Pandas,还是 SQL、DolphinDB,这条原则几乎都成立。原因并不复杂:向量化能够将大量元素级计算交给底层高性能实现,在一次函数调用中完成整个数据批次的处理,从而避免解释器反复执行循环所带来的运行时开销。

因此,在大多数数据处理场景中,向量化始终是性能和开发效率兼顾的最佳选择。

但现实业务,并不总是如此。

越来越多企业计算开始包含复杂的控制流逻辑:策略回测需要根据上一笔成交不断更新持仓状态;实时风控需要维护账户状态并根据不同条件触发处理逻辑;CEP(复杂事件处理)需要随着事件流持续更新状态;金融定价、数值求解则需要不断迭代逼近结果……

这些计算有一个共同特点:当前计算依赖此前的中间状态,而不是简单地对每个元素执行相同的计算。

这意味着,将它们强行转换为向量化表达,往往会增加实现复杂度,甚至未必能够获得更好的性能收益。

那么,当向量化覆盖不到的时候,还有没有另一条性能优化路径?

为什么解释执行的循环容易成为性能瓶颈?

很多人认为,循环慢,是因为 CPU 执行循环效率低。实际上,在很多解释型语言中,真正的开销往往并不来自循环本身,而是解释执行过程。

每一次循环迭代,都可能涉及变量解析、动态类型检查、函数调度、对象访问等运行时工作。当这些操作需要重复执行数百万次时,解释器带来的额外开销,甚至可能超过真正的数据计算。

因此,在对性能要求极高的场景中,一种常见做法,是将热点逻辑迁移到 C++ 等编译型语言实现。

这种方式确实能够获得更高性能,但同时也意味着新的成本:维护多语言工程、编译动态库、跨语言接口,以及随业务演进不断增加的维护复杂度。

有没有一种办法,既保留脚本开发效率,又能够获得接近原生代码的执行速度?

这正是 JIT(Just-In-Time,即时编译)试图解决的问题。

JIT 改变的,不是算法,而是代码的执行方式

很多人第一次接触 JIT,会误以为它是一种新的计算优化算法。事实上,JIT 改变的并不是算法本身,而是代码的执行方式。

普通脚本运行时,代码通常由解释器逐句执行;而 JIT 会在程序运行过程中,将热点代码编译成本地机器码,并缓存编译结果,后续再次执行时便可以直接运行机器码,而无需重复经过解释执行。

因此,从开发者的角度来看,业务逻辑没有改变;真正变化的是背后的执行链路:这段代码不再依赖解释器逐句执行,而是以更接近原生程序的方式运行。

从 Java HotSpot、.NET CLR,到 V8、PyPy、Julia 等运行时,都采用了类似的思想,只是在热点识别、编译框架和优化策略上各有不同。

对于循环、分支、状态更新等解释执行开销占比较高的计算,JIT 往往能够显著降低运行时开销,因此成为许多高性能运行时的重要组成部分。

为什么 DolphinDB 要重构 JIT?

此前的 JIT 实现在应对复杂控制流和多层类型推导时,优化空间相对有限。这次重构选择 MLIR 作为底层编译框架,正是为了构建更灵活的编译优化基础。MLIR 提供了更丰富的中间表示和优化能力,使 DolphinDB 的 JIT 能够更好地支持类型推导和控制流优化,也为未来扩展更复杂的语法特性和更多数据类型提供了基础。

但需要说明的是 JIT 并不是用来替代向量化。对于聚合、筛选、窗口计算等数据分析任务,向量化始终是效率最高、代码最简洁的方案。我们并不希望开发者为了使用 JIT,而放弃原本更优雅、更高效的向量化表达。这次重构补齐的,是向量化覆盖不到的那部分计算,让那些无法向量化的复杂业务,同样能够获得接近原生代码的执行效率。

或者说,两者分别解决的是两类不同的问题:向量化解决的是数据并行,JIT 解决的是流程控制。当计算能够表示为“对整批数据执行相同操作”时,向量化通常是最佳选择;而当业务逻辑天然依赖循环、分支、状态更新等控制流时,仅靠向量化就不再适用,JIT 则提供了另一种高性能实现方式。

JIT 的使用方式非常简单:只需在函数或类定义前添加一个 @jit 注解即可。需要注意的是,当前这一版 JIT 对运行环境有一定要求,需要 server 满足对应的 ABI 版本才能使用,具体要求可参考官方文档说明。

代码语言:txt
复制
@jit 
def positiveSum(v) { 
    total = 0.0
    for (x in v) {
        if (x > 0) {
            total += x
        }
    }
    return total
} 

注:这里只用于展示 @jit 的基本写法。对于这类简单过滤求和,实际业务中仍然建议优先使用向量化表达。

开发者几乎无需调整原有代码,就可以将热点函数交由新的 JIT 编译器处理。

哪些场景最适合使用 JIT?

并不是所有代码都适合开启 JIT。如果一个计算本来一句 sum() 就能完成,那么向量化通常仍然是最佳选择。真正值得使用 JIT 的,是那些解释执行成本远高于计算成本的热点逻辑,例如:

  • 大量 for、while 循环;
  • 包含复杂 if/else 分支的业务规则;
  • 不断更新对象状态或字典内容;
  • 路径依赖计算、数值迭代求解;
  • 实时 CEP 事件处理、高频策略回测中的状态更新等。

这些场景共同的特点是:很难改写成向量化表达,但又会被重复执行成千上万次。

下面用两个具体案例说明。本次测试基于 Ubuntu 20.04 系统(内核 5.15),CPU 为 i5-10400F(6 核 12 线程)。

案例一:计算隐含波动率

隐含波动率通常不存在解析解,需要通过迭代逼近求得,例如采用二分法在给定区间内不断收缩范围,直至误差满足精度要求。每一步计算都依赖上一步收缩后的区间,这种路径依赖的逻辑难以用向量化表达,只能以循环方式实现。

代码语言:txt
复制
@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 次,结果如下(正确性校验通过):

案例二:计算止损点

止损判断同样是典型的路径依赖计算:当前收益需要与历史最高点持续比较,一旦回撤幅度超过阈值即应立即返回,无需继续遍历后续数据。这种“边计算边判断、可能提前退出”的逻辑,同样难以用向量化方式表达。

代码语言:txt
复制
@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?

除了以上两个案例,在最基础的循环求和测试中,完成首次编译预热后,JIT 版本相比普通解释执行获得了超过 55 倍的性能提升。

当然,这并不意味着任何代码都会获得几十倍提升。JIT 的实际收益与代码结构、数据规模、参数类型稳定性等因素密切相关。因此,更合理的做法,是根据业务特点选择合适的实现方式,而不是将 JIT 作为所有场景的默认选择。

也有几类场景并不适合使用 JIT:

一是已经能通过向量化高效完成的计算。例如简单的聚合、筛选、加减乘除等,向量化已经足够高效,强行使用 JIT 不仅无法带来明显提升,反而可能因为编译开销而得不偿失。

二是仅执行一次或很少执行的冷代码。JIT 的优势在于“编译一次,执行多次”。如果一段代码只运行一次,编译开销甚至可能超过直接解释执行的耗时。

三是参数类型频繁变化的热点函数。JIT 会针对不同的参数类型分别生成和缓存机器码,频繁切换类型会导致重复编译,抵消 JIT 带来的收益。

判断是否适合使用 JIT,本质上是一个简单的权衡:编译开销 + 机器码执行时间 < 解释执行时间。只有当代码被执行足够多次,且执行时间远大于编译时间时,JIT 才能发挥真正的价值。


此次基于 MLIR 的 JIT 重构,并不是为了替代向量化,而是希望让更多无法自然向量化的复杂业务,同样能够获得高性能的执行能力,是对复杂控制流计算能力的重要补充。向量化负责数据并行,JIT 负责流程控制,两者相辅相成,共同构成了 DolphinDB 更加健全、更具扩展性的脚本执行引擎。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 向量化不是万能的
  • 为什么解释执行的循环容易成为性能瓶颈?
  • JIT 改变的,不是算法,而是代码的执行方式
  • 为什么 DolphinDB 要重构 JIT?
  • 哪些场景最适合使用 JIT?
    • 案例一:计算隐含波动率
    • 案例二:计算止损点
  • 哪些场景不适合 JIT?
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档