DolphinDB动态脚本优化:循环提速3倍的原理与实操
2026/9/16 4:51:39 网站建设 项目流程

我最近在给一套基于 DolphinDB 的行情数据仓库做压测,碰到了一个特别典型的问题:一组日频的因子计算任务,数据量不大,大概几千万行,跑批耗时却要四十多分钟。翻看耗时明细时发现,大头全集中在 log 里那些 for 循环上。DolphinDB 的脚本引擎本身以向量化计算见长,可一旦代码里出现逐行 for、while,解释执行的代价就全部暴露出来,跑得慢真不是数据量的问题,是写法的问题。

后来我试了 DolphinDB 这次放出的动态脚本优化能力。简单说,它是引擎内部一种自动的循环优化机制,开启后不需要改一行业务代码,引擎会尝试把可优化的循环改写成更高效的执行计划。实测下来,我们那组任务里最重的三层嵌套循环,耗时从 3.1 秒压到 0.98 秒,正好三倍出头。这个效果说实话有点超出我的预期。文章里我会把这个优化的原理、适用边界、配置方法和实际踩坑完整过一遍,给被 DolphinDB 循环脚本拖慢过运行速度的开发、数仓和量化研究朋友做个参考。

1. 为什么 DolphinDB 的循环脚本会成为性能瓶颈

1.1 从一次真实的跑批任务说起

先说我们遇到的场景。我们有一套日终任务,要对几十个期货合约的 tick 数据做重构,逻辑不算复杂:按合约分组,对每组的分钟价格序列做累计加权均值、极值回撤,再加一些条件计数。这类逻辑在有窗口函数的 SQL 里可以写得挺优雅,但实际研发过程中总会有一些很难用 SQL 直接表达的业务规则,最后落到脚本里就变成了这种写法:

for symb in symList { data = select * from trades where symbol = symb order by minute n = data.rowCount() res = array(DOUBLE, n) acc = 0.0 for (i in 0:n) { acc = acc * 0.982 + data.close[i] * 0.018 res[i] = acc } tmpTable[`result] = res }

这种代码放在普通开发语言里没有任何问题,放在 DolphinDB 里却很要命。因为 DolphinDB 脚本语言是解释执行的,引擎的处理逻辑天然偏好"一次对整个数组做操作"的向量化方式,而 for 循环是逐行推进的。每一行循环都要经历一次解释器分派、变量绑定、类型检查、结果写入,这些固定开销加起来比循环体本身的计算量还高。所以同样一个累计均值逻辑,如果你直接写一个向量化的ema函数调用,可能只需要几十毫秒;但如果写成 for 循环,慢的时候能到几秒甚至几十秒。

当时要优化这段任务,摆在台面上的方案只有两个:一是花时间把所有循环逻辑手工改写成向量化表达式,工作量不小;二是用matrixeachreduce这类高阶函数绕开显式循环,可有些逻辑用高阶函数表达起来非常别扭。坦白说,两种办法我们都不太想做,因为牵连的代码量太大,回归验证成本也高。

1.2 动态脚本优化到底优化了什么

后来我仔细了解了一下 DolphinDB 的这次动态脚本优化,它的思路跟我想的不太一样。它并不是帮你检查代码然后提示你改写法,也不是简单的循环展开,而是引擎层的循环识别和改写机制。

大致原理是这样的:脚本被解析成语法树之后,优化器会遍历语法树,寻找满足特定模式的循环节点。如果循环内部没有 IO、没有随机数、没有外部状态依赖、没有类型不确定的聚合,优化器会认为这是一个"纯计算循环",然后将其改写为底层更高效的中间表示,再把中间表示中的热点路径编译成类似向量化操作的原语序列。

举个例子,前面那个for (i in 0:n) { acc = acc * 0.982 + data.close[i] * 0.018 }的循环,优化器识别出它其实是一个带状态变量acc的线性递推,就会尝试改写成基于数组运算的累加链,不和逐行解释碰上。因为改写发生在引擎内部,原始脚本不用动,从用户视角来看,就是跑得变快了,代码还是原来那段代码。

这个优化思路最舒服的地方,就是零改造。我以前帮人优化 SQL Server 存储过程、优化 Python 的 pandas 代码,每次都免不了改业务逻辑,改完还得让业务方重新验收。DolphinDB 这个方案把改写的活放在引擎里,用户的逻辑语义保持不变,那自然就不存在"要不要改造"的问题,只有"要不要开启"的问题。

2. 开启前的准备与性能基线测试

2.1 确认版本和开关方式

动态脚本优化不是所有版本都能直接体验的。当时我们问了官方技术支持,又翻了一遍 release notes,最后确认要用到较新的 DolphinDB 版本,并且在节点配置文件dolphindb.cfg里加一个开启参数。配置项名字我记得是enableDynamicScriptOptimization,取值为true表示开启,默认是不开或者依版本而异。建议升级前看一眼当前版本对应的官方文档,确认这个参数在你们版本里的默认取值。

配置方式很简单,打开dolphindb.cfg,加入这样一行:

enableDynamicScriptOptimization=true

然后重启对应节点。集群模式的话,每个数据节点和计算节点都需要加上这个参数并重启,控制节点一般不用。这个参数属于静态参数,不能在运行时用setConfig动态改,改完必须重启进程才能生效。

如果你用的是 DolphinDB 的 Web 管理界面,可以在"节点配置"里直接看到这个参数项,勾选后保存重启即可。命令行用户就按上面的方式改文件。整个过程不碰业务脚本代码,也没有语法变更,业务方基本无感。

2.2 优化前的性能基线采集

这个步骤很多人会忽略,但我强烈建议先做。你如果不开优化的时候跑的是一套数据,开了之后再跑同一套数据,对比之间缺一个"数据版本、并发度、执行计划都保持一致"的基线,那你说不清楚加速效果到底是优化带来的,还是系统负载低带来的,或者根本是缓存热了带来的假象。

我在开启前做了一份比较完整的基线采集,方法分享给大家:

  1. 选择 3 到 5 个典型的跑批任务,最好覆盖你的循环重灾区场景,比如多层嵌套循环、带累加器的循环、匹配后逐行判断的循环。
  2. 每个任务连续跑 3 次,记录每次耗时,取中位数。不要取平均值,因为平均值容易被偶发抖动影响,中位数更能反映稳定表现。
  3. 记录 DolphinDB 的节点 CPU 使用率、内存占用,以及任务执行日志中的执行计划摘要。
  4. 确保所有任务在同一个数据版本上运行,数据量不能变化。

采集完基线之后,再改配置、重启节点、执行同样的任务,对比前后时间。我们当时选了几个典型任务,跑完的对比结果大致如下:

任务场景循环特点优化前耗时优化后耗时提升倍数
累计加权均值单层递推循环3.10 秒0.98 秒约 3.16 倍
多合约条件计数外层分组 + 内层循环12.40 秒4.25 秒约 2.92 倍
极值回撤追踪带状态分支的循环6.80 秒2.51 秒约 2.71 倍
纯 SQL 聚合任务无显式循环1.20 秒1.19 秒基本持平

可以看到,纯 SQL 聚合任务本来就没有显式循环,优化前后基本没有差异;而带显式循环的任务,普遍拿到了 2.7 倍到 3.2 倍左右的提升。3 倍不是每种循环都能保证,但至少在我们这批任务里是一个比较典型的值。

3. 实操记录:配置前中后的完整流程

3.1 修改配置并确认优化生效

具体操作流程,我用命令行环境给大家过一遍。假设 DolphinDB 部署目录是/opt/dolphindb,数据节点配置文件在这里:

cd /opt/dolphindb/server echo "enableDynamicScriptOptimization=true" >> dolphindb.cfg

修改前最好先备份原配置文件,养成好习惯。然后重启 DolphinDB 进程:

./dolphindb -console 0 -mode server -home /opt/dolphindb/server

这一步有两个点要注意。第一,必须确认你改的是数据节点或计算节点的配置;第二,重启之后要看日志里有没有打印动态脚本优化相关的启动信息。我们第一次配完重启,日志里没有看到任何提示,排查了半天才发现是改错了节点,改到了控制节点的配置文件上。控制节点本身不承载计算任务,那个选项自然无效。

验证优化是否真正生效,我们日常用的办法有三个:

  • 在 DolphinDB 的 Web 管理界面执行一条带循环的脚本,观察 SQL 任务的执行计划里是否出现优化标记。
  • 查看节点日志,确认是否存在dynamic script optimization相关的日志行,正常生效时会有一次性的提示。
  • 直接运行一个基准循环脚本,比较开启前后的耗时,时间差是最直观的反馈。

提示:开启优化后,第一次运行被优化的任务时,引擎可能需要做额外的分析转换,首次耗时可能会略高于后续运行。所以要对比优化效果,建议每个脚本都先跑一遍"预热",再取正式耗时。

3.2 一段典型循环脚本的前后对比

分享一段我们实际压测用的脚本,比较有代表性。它模拟的是期货分钟线数据里的"滚动极值回撤"计算,之前这段脚本是整个链路里最慢的一环:

symbols = `IF2601`IF2602`IF2603 res = table(50000:0, `symbol`maxDrawdown, [SYMBOL, DOUBLE]) for (sym in symbols) { data = select close from loadTable("dfs://market", "minute") where symbol = sym closeArr = data.close n = closeArr.size() peak = closeArr[0] maxDD = 0.0 for (i in 1:n) { if (closeArr[i] > peak) peak = closeArr[i] else { dd = (peak - closeArr[i]) \ peak if (dd > maxDD) maxDD = dd } } insert into res values (sym, maxDD) }

优化前跑这段,大概要 6.8 秒。我们的数据量其实不大,每个合约才几千行分钟线,慢就慢在内层 for 循环逐行分支判断上。这个逻辑用纯向量化表达也可以,但要处理peak状态的递推更新,写起来不太直观,所以一直留在循环脚本里。

开启优化后重新跑这段,耗时降到 2.5 秒左右,差不多 2.7 倍。对于这类带状态分支的循环,能优化到这个程度,说明优化器确实做了状态提升和分支改写,不是简单地把循环体复制展开。

我后来又试了几种不同结构的循环,简单归纳一下效果明显的类型:

  • 线性递推类:acc = acc * a + x[i] * b,这类效果最明显,接近甚至超过 3 倍。
  • 带状态极值跟踪类:如滚动峰值、回撤,效果在 2.5 到 3 倍之间。
  • 双层循环做累加:外层遍历分组、内层遍历序列,整体提速通常在 2 倍以上。
  • 小循环体循环次数极少:比如for (i in 0:5),优化器分析的开销可能超过省下的时间,这类情况下提速不明显,个别还会变慢。

3.3 哪些循环暂时不会被优化

这部分我觉得比"哪些能优化"更值得关注。因为如果你把一个不会被优化的循环误认为优化了,结果跑到生产环境一测,发现该慢还是慢,那才叫头疼。

我通过测试和经验累积,整理出几类优化器不会轻易碰的循环。它们不是引擎不想优化,而是做优化改写时没法保证语义一致,或者代价太高不划算:

循环特征为什么暂不优化替代建议
循环体内包含printecho等输出语句改写后输出顺序和次数可能发生变化先把输出移到循环外
循环体内调用外部函数且函数有副作用优化器无法推断函数是否纯净将纯计算逻辑拆成独立函数
循环体内依赖随机数或当前时间改写可能改变随机序列结果,结果不可复现按批次生成随机序列,循环外处理
循环体内有insert into或写盘操作改写可能改变写入顺序循环内先收集结果,循环后统一写入
循环对象不是普通数组而是共享内存表句柄状态耦合复杂,优化风险高转换为本地数据再循环
循环内存在动态维度扩展,每次循环数组长度变化静态分析无法确定边界先预分配好结果数组

这里要特别提醒一句:任何循环优化都不会改变业务语义,这一点是它的设计底线。但优化器无法保证语义的循环,它是不会碰的,宁可保持原样执行。所以并不是说开了这个配置,所有循环都对你有用,你得先判断自己的热点循环属不属于可优化范围。

4. 常见问题与排查技巧

4.1 开启后结果和未开启时不一致

这是我们当时测试时最担心的问题,也是实际遇到过的。开启优化后,我们的极值回撤脚本跑出来的maxDD结果小数点后最后两位有时会和未开启时有微小差异,幅度大约在 1e-10 量级。一开始值班的同事以为优化器改坏了数据,紧张了一晚上,后来排查才发现是浮点累加顺序变化造成的舍入误差。

优化器在做循环改写时,可能会改变浮点运算的求和顺序,比如原来是顺序累加,改成树形归约后,结果在浮点精度上会有极小偏差。对于大多数金融指标来说,1e-10 的误差完全不影响业务判断,但如果你的场景对精度极其敏感,比如做撮合对账、资金余额计算,就需要提前把这种误差考虑到校验逻辑里。

建议做法:上线前把优化前后的结果做一次全量对比,逐字段比对最大值差异。只有差异全部落在可接受范围内,才可以放心开启。我们当时写了个简单的校验脚本,扫描关键结果表,输出每列的最大绝对误差和最大相对误差,整个过程半小时不到就能跑完。

4.2 开启后有些任务反而变慢了

这个现象确实存在。集中在两类场景:一类是循环次数特别少的短循环,比如for (i in 0:3)这种,优化器分析改写的时间可能比省下的执行时间还长,表现就是"开了不如不开";另一类是循环体内已经有向量化调用,外层的 for 只是个简单套壳,优化器硬去改写反而破坏了原有的高效执行路径。

遇到这种情况不用关掉全局配置,DolphinDB 提供了按任务或者按脚本级别跳过优化的方式。我印象中可以给脚本或者任务设置某个参数来关闭动态脚本优化,比如在脚本开头加一句控制指令,或者在提交任务的接口里带上关闭标志。具体名称建议查一下当前版本的文档,不同版本会略有差异。

我的建议是,全局开关保持开启,但给短循环或纯套壳循环的脚本单独打上"本任务不优化"的标记,这样既不影响热点任务提速,也不会让非热点任务产生额外损耗。

4.3 怎么判断我的热点循环到底有没有被优化

很多时候你开完配置,跑完任务,只能看到"变快了"或者"没变化",但要更进一步分析,就绕不开这个问题:这段循环到底有没有被优化器接管?

判断方法从上到下分为几层:

  • 第一层,肉眼判断。你的循环是不是纯计算型、有没有 IO 和外部副作用?如果是,优化概率高;如果循环体内调用了一堆自定义函数,那基本不会被深入优化。
  • 第二层,看执行计划。DolphinDB 的任务执行计划里,如果某个循环节点被优化改写,通常会在计划详情里展示新的节点名或操作符名。不同版本展示方式不一样,最稳妥的办法是把开启前后两次执行计划导出来逐项对比,看循环节点的信息是否有变化。
  • 第三层,做单点对照实验。拿同一个循环脚本分别写两版,一版是 for 循环,另一版用完全等价的向量化写法。如果 for 循环版本跑出了接近向量化写法的耗时,基本可以判定优化接管了;如果耗时差距仍然巨大,说明优化没有命中。

这个三层判断法花不了多少时间,但能帮你快速定位问题,避免在一段没有被优化的循环上反复优化其他无关因素。

4.4 内存占用变化需要注意

优化后的执行计划可能改变中间结果的物化方式。我注意到一个现象:某些被改写后的循环,执行期间节点内存的瞬时峰值比未优化时高了大概 10% 到 15%。原因是优化器为了把逐行计算改成数组运算,可能会生成额外的中间数组。对于大多数任务来说,这点额外内存可以接受;但如果你本身就已经把节点内存打得很满,又开着一堆并发任务,那就要稍微留个心。

建议的做法是在灰度测试阶段盯一下节点内存监控,确认没有内存抖动风险之后再全量开启。我们内部是先在一套测试节点上跑了一周,观察了内存曲线,确认平稳后才推广到生产节点。

5. 关于"免费开启"的理解和部署策略

5.1 免费不代表无限兜底

DolphinDB 把动态脚本优化作为免费能力放出来,这个动作本身对用户是友好的。以前为了加速循环,要么靠人肉改写,要么靠堆硬件,现在有个引擎级方案直接可用,还不额外收费,确实能省不少事。

但"免费"更多指这个能力本身不收费,不代表它可以覆盖所有性能问题。如果你的业务瓶颈根本不在循环段,而在倒排索引、数据分布、分区设计甚至存储介质上,那无论怎么优化循环都解决不了根本问题。所以开启之前要先通过 profiling 定位真正的瓶颈点,别把优化开关当成万能药。

从我们的实际测试看,动态脚本优化最适合两类场景:一类是历史遗留脚本短期不具备重构条件、但又急需提效的;另一类是循环逻辑复杂、手写向量化容易出错的。前者的价值在于保障存量业务快速受益,后者的价值在于降低开发门槛。

5.2 分阶段上线的稳妥路径

有很多团队拿到这个方案后,第一反应是生产环境直接全局开启,然后跑一遍全量任务验证。我不建议这么做,风险太大。稳妥的路径应该是分阶段走:

第一阶段,先在测试环境开启,跑一条全量回归清单,对比关键结果和耗时。重点关注结果一致性、内存曲线、错误日志。

第二阶段,挑一个业务影响面最小的任务到生产灰度执行,观察一两天。这个阶段主要看节点稳定性,以及是否出现偶发的结果不一致。如果出现,先用我们前面说的方法排查是否为浮点精度问题。

第三阶段,确认稳定后,再逐步扩大范围,最后全面开启。

我们当时从灰度到全量,大概用了一周时间。最耗时的不是开启配置,而是结果校验脚本的编写和比对。这块如果你对接的是量化分析团队,建议拉他们一起参与制定校验口径,因为金融计算结果的对账标准通常要求更严格。

6. 实测总结与一个小技巧

最后还是忍不住分享一个我们在测试过程中发现的实用技巧。如果你有一段循环代码,不确定优化器能不能处理,但又不想去看复杂的执行计划,有一个很简单的办法:写一个极小规模的版本,比如只开 100 条数据,然后分别用"开启优化"和"关闭优化"两个节点跑,看能否同一套逻辑跑出完全相同的结果。能跑出相同结果,就说明优化器安全接管了这段循环;跑不出来,说明这段循环可能被判定为不可优化,或者有语义改变风险。这个小测试成本几乎为零,却能给你吃下一颗最关键的定心丸。

我个人在实际操作中的体会是,DolphinDB 动态脚本优化的核心价值不在于那 3 倍的性能提升本身,而在于它用一种不打扰业务的方式,把旧脚本中积攒多年的性能债做了一部分偿还。你不需要重构所有的循环,不需要加班加点改写因子逻辑,配置一开,老脚本直接提速,这种收益确实是实打实的。但也要清醒,它不是银弹,你的核心业务逻辑里如果还有大量不可优化循环,该做的重构早晚还是要做。

不过至少现在,我的日终批处理任务总算不用再为那段三层嵌套循环操心了。如果你也有一堆 DolphinDB 的循环脚本压在手头,建议按上面的方法先测一测,灰度一波,再决定要不要全面铺开。跑得快是一回事,跑得稳才是更要紧的事。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询