Node 返回对象的数值后来变了:排查 Maglev OSR 路径
多次调用得到不同对象,先前保存的浮点字段却随后续调用改变。限定已知 Node 与架构条件,用有界优化开关对比保留业务输入。
函数每次都返回新的对象,后一次调用结束后,前一次结果的 area、maxOff 却变成了新值;对象引用仍不相等,其他 Set 字段也各自独立。若问题只在部分进程中出现,且运行 Node.js 24.11.0 或 26.9.0,可以检查 Maglev 的 OSR 优化路径。
已确认的场景涉及 macOS ARM64、循环中的浮点计算、返回对象字面量以及调用之间的分配活动,26.10.0 也有相同确认。数值漂移、共享对象或异步覆盖还可能来自应用代码;先排除应用确实复用结果对象、共享缓冲或回调修改状态。
保存返回时的标量,而不是再复制一个引用
在受控检查里,将对象和刚返回时读取的数值分开保存,等后续调用后比较:
const result = calculate(input);
const saved = { result, area: result.area, maxOff: result.maxOff };
// 完成其他原有计算后,比较 saved.result.area 与 saved.area。
calculate 和 input 指实际函数及原输入,这段只说明记录方式。数值按值保存才能发现后续变化;const saved = result 仍是同一个对象引用,无法形成对照。存在 NaN、正负零时,比较可使用 Object.is,避免普通相等掩盖边界。
这条已知路径中,不同对象的双精度字段可能共享底层 HeapNumber。后续写入于是影响先前对象,输出并非简单的小数舍入误差。出现重复 Maglev 反优化日志可作为定位线索,但日志形状本身不证明结果已损坏。
临时禁用 Maglev OSR,需要保留同一组计算
对符合这些条件的进程,可以在受控启动中比较:
node --no-maglev-osr app.mjs
app.mjs 替换为原入口,保留原参数、环境和输入。这项 V8 开关关闭以 Maglev 为目标的栈上替换路径,会改变优化行为,可能影响性能;不应未经业务回归就扩展到整台机器所有 Node 任务。使用子进程或测试 worker 时,要确认开关确实到达实际执行计算的进程。
不能把延长等待、减少并发或增加垃圾回收当作恢复证明:这些调整会改变触发时机。给结果做浅复制,也没有修正原对象后续读值错误的来源;已有消费者还可能持有旧引用。
当前正式 Node 26.12.1 的发行清单捆绑 V8 14.6,不能因为另一条版本线采用更新的 V8,就认定当前 24、26 系列已经包含相应修正。选择维护版本时核对其正式内容,再对原调用形状回归;不要只比较大版本数字。
验收要保留浮点字段、对象形状变化、调用间分配与原并发条件,既确认旧结果没有改变,也确认每次计算仍得到正确值。记录尝试预算和观察结果,有限次数未出现不能证明任何输入都安全。若过去已保存错误结果,应先按业务记录核对,运行时选型不会自动更正历史数据。


