两条跑道的计时器读数相反:一条在发令枪响时领先,另一条在跑到后半程被反超

一、两个指标反向走,说明它们测的根本不是同一件事

据公开报道,有团队发布了一款实验性编译器,能把普通 TypeScript 转成小型原生可执行文件,对比数字是启动快约十二倍、运行慢约七倍半。这两个倍数在这里只用来说明一件事:同一份代码、同一次评测,两个指标可以朝相反方向走,所以“它更快了”这种结论根本不成立。它们不是我们下面判断的依据,也不能被拿去当任何选型结论。

真正的问题是评测口径。启动时间和稳态运行时间是两类完全不同的负载特征:前者是一次性开销,发生在进程生命周期最前面那一段,主要由运行时初始化、模块解析、依赖加载构成;后者是持续开销,取决于代码被执行了很多遍之后会发生什么。把两者混在一个“性能”标签下报出来,读者会自动补一句“总体更快或总体更慢”,而这句补充恰恰是错的。选型该问的不是“哪个数字大”,而是“我的负载里哪一段占主导”。

我们内部评审这类工具时,第一条规则是禁止在报告里出现单个综合性能数字,必须按阶段拆开:进程创建到能接受第一个输入、单任务从输入到输出的处理耗时、以及长时运行下的吞吐与内存形态。三段各自有各自的结论,允许出现“这段赢、那段输”的表述。

二、编译到原生为什么在启动这一步占便宜

启动路径上的开销大致四类,都能对上明确的机理。

  • 运行时初始化:解释型运行时在跑第一行业务代码之前,要搭好堆、内部对象、内置模块注册表,这部分和业务无关,却每次进程启动都要重付
  • 即时编译预热:热点代码要先按较低效率执行,边执行边收集信息再编译,启动阶段基本享受不到优化成果
  • 模块解析与加载:包管理目录结构下的路径查找、循环依赖检测、逐个模块编译,在依赖树大的项目里是一段实打实的时间
  • 依赖体积:加载的代码越多,解析与校验越多,冷启动越慢

编译到原生把这些挪到了构建期:产物是一个已经处理好的可执行文件,进程创建后可以直接进入业务。所以启动上的便宜不是“代码写得更快”,而是“该付的固定成本不再在每次运行时重付”。这一点对短生命周期进程特别友好,因为它本来就没几行业务代码可以摊薄这份固定成本。

三、运行阶段为什么可能反过来

这一段只讲一般机理,不推断那个项目的实现方式。

即时编译的价值出现在“同一段代码被执行很多遍”之后:它能根据真实运行数据做专门优化,包括把多态点改成单态、内联、按实际类型布局。一次编译到原生的产物拿不到这种反馈式优化,或者只能拿到较弱的版本。

动态语义的兜底实现是第二项代价。带动态类型语义的语言里,属性访问、原型链、动态扩展这些行为在运行时有很成熟的快速路径。编译型产物要么把这些行为翻译成一层通用查表逻辑,要么把优化空间收窄到保守假设上,两种都让单次操作变贵。

第三项常被忽视:成熟运行时的内建函数是几十年调优的结果。字符串处理、正则、集合、数值转换、编码转换,这些在真实业务里出现频率极高,而编译产物要么重新实现一遍、要么把调用绕回一个宿主运行时,两条路都谈不上优势。

于是结论自然出现:这类技术的收益边界不在“快慢”,而在生命周期长短与执行遍数多少。所以也不存在“编译到原生会取代运行时”这种判断——两条路线各自服务不同的负载形状,这个论点在任何评测方法下都站不住。

四、四类负载分别该看哪个指标

核心交付物是这张分档表。它的作用是把“看哪个数”定死,避免拿错指标下结论。

负载形状 典型形态 主要开销在哪 该看的指标 不该看的指标
一次性进程 命令行工具、内部脚本、CI 里跑一次就退出的任务 固定启动成本占大头,业务代码跑几遍说不清 端到端完成时间分布、进程创建到首个输出的时间 峰值吞吐
常驻服务 长时间运行的接口进程 启动成本每天只付几次,运行期占绝对大头 稳态处理耗时与吞吐的分位数、内存随时间形态、GC 或回收造成的停顿形态 单次启动时间
冷启动敏感的边缘形态 按请求拉起、生命周期很短、并发量随时段波动 启动成本直接进用户可见延迟,且没有预热机会 冷启动端到端时间、并发扩容时的排队时间、按调用计费涉及的资源占用 稳态吞吐
长时批处理 跑几十分钟到数小时的转换、导入、训练数据准备 运行期主导,启动可忽略 单位数据量的处理耗时曲线、长时间运行后的稳定性与内存上限 首包时间

判定的方法很朴素:先写出这个进程每天被创建多少次、每次活多久。活的时间短、创建次数多,启动成本就占主导;活的时间长,运行成本压倒一切。这一句话就能把上面四档分开,不需要跑基准。

把公开报道里的倍数还原成业务问题,也是这三问:谁在等这段启动时间(用户、构建流水线还是下游任务);每天启动多少次;单次运行多久。三问答完,性能数字才有方向。以洹拓接手过的存量项目来看,多数团队的命令行与流水线脚本能从启动优化里拿到可感知的收益,常驻服务拿不到;但这不是选型结论,因为决定它的是上面三问的答案。

五、测法怎么设计才不失真

评测方法错了,后面所有决定都错。七条纪律:

  1. 固定输入集:评测用例、依赖版本、机器与系统状态全部冻结,输入不固定的对比只用于讲故事
  2. 预热轮次与丢弃规则:先约定丢弃几轮预热,并写明“丢弃”不等于“不报告预热本身耗时”
  3. 首包与稳态分开记:一条时间轴上两个数字,绝不合并成一个平均值
  4. 用分位数而不是均值:至少记中位数、高分位与最差值,长尾恰是这类工具最容易吃亏的地方
  5. 记录资源占用而不只是时间:内存峰值、磁盘与产物体积、并发下的退化情况
  6. 保留可复现产物:评测脚本、提交标识、构建参数一起存档,别人跑不出来就不进评审
  7. 与真实业务同构:拿玩具基准得出的结论只能指导玩具决策,要按第四节的负载形状各准备一条真实任务

评测结果进决策文档时,必须同时写下负载假设。假设不成立的那天,这份结论就作废,而不是继续留在知识库首页当标准答案。

六、实验性工具的六项边界检查

这类工具值得跟,但“跟”的含义是先进工程边界清单,不是先进生产默认链路。六项检查建议作为引入前的门槛,任何一项标不过就先不进主干。

检查项 要通过什么 通不过的后果
兼容矩阵 明确支持的语法特性、标准库与第三方模块范围,且我们代码里用到的每一项都在列 构建期或运行期出现无人能解释的失败
调试符号与栈信息 线上问题能拿到可读栈、能定位到源码位置 只能靠复现猜,故障处理时间不可控
崩溃诊断 崩溃有分类、有可采集的现场信息、有上报通路 值班无法判断影响面,只能整体回滚
构建产物可复现 同一提交在任意机器上产出一致,构建依赖被锁死 出现“本地能编、线上编不出”的长期扯皮
产物体积与分发 各平台产物大小与分发成本可接受,能纳入现有发布流程 下载与更新链路变慢,用户端升级失败率上升
许可证与供应链、退出成本 许可证义务能履行(宽松许可证不等于没有义务:编译产物里带不带第三方代码、要不要随附声明与副本要单独核);上游停维护时的替代路径与迁回原运行时的改动量被评估过 工具换代时业务代码要重写一遍

最后一项里藏着真正的风险:代码是否被绑定到特定编译约束。有些写法在原生运行时里完全正常,进了编译链就要改写;一旦这类写法在业务代码里扩散,迁走就不再是改构建脚本,而是改业务。护栏是把这类约束列成显式规则,在评审里当作可检查项,而不是靠口口相传。

我们的做法倾向是:把它先放在一次性进程与内部工具这一档,跑一段时间的真实任务,攒够上面六项的证据,再谈是否往服务链路挪。这既不是推荐也不是否定,只是把结论留给负载形状。