Bun 1.4实测:Rust重写将启动时间从10.5ms压缩至4.2ms

原文:https://dev.to/alexgeorgiev17/bun-14s-rust-rewrite-cuts-script-startup-time-from-105ms-to-42ms-2je0(作者 @alexgeorgiev17)

Bun 1.4 于 8 月 20 日发布,是首个基于全新 Rust 核心构建的稳定版本,取代了自 1.0 以来一直使用的 Zig 运行时层。发布说明宣称其启动更快、空闲 CPU 占用更低、内存占用更少。我安装了重写前的最终版本(1.3.14)和新版本(1.4.2)进行并排测试,验证了以上三项指标,并额外测试了将 node 符号链接指向新二进制文件时的行为——这也是许多人在生产环境中运行 Bun 的实际方式。

我使用官方安装脚本并指定版本标签,将 Bun 1.3.14 安装到 ~/.bun-old,将 1.4.2 安装到 ~/.bun-new,这样两个二进制文件位于同一台机器上,我可以直接调用任意一个。

启动时间

我的第一轮测试是对每个版本的 bun --version 命令计时二十次。两者几乎没有差别:1.3.14 的中位数为 2.24ms,1.4.2 为 2.00ms。该命令在初始化 JavaScript 引擎之前就会退出,因此它并未测量发布说明所描述的内容。当我切换到实际运行脚本时,差距才显现出来。

console.log("hi")


分别对 bun run hello.js 执行三十次,并使用 time.perf_counter()subprocess.run 调用周围计时:

  • Bun 1.3.14:中位数 10.54ms,最小值 9.32ms,最大值 18.00ms
  • Bun 1.4.2:中位数 4.22ms,最小值 3.90ms,最大值 5.25ms

这意味着中位数启动时间降低了 60%,优于发布说明中声称的“在 Linux 上快 50%”。旧版二进制文件的尾部延迟也长得多(最高达 18ms),而新版在所有 30 次运行中都保持在 1.4ms 的带宽内。

空闲内存

我在每个版本上运行一个两行的 Bun.serve 回显服务器,访问一次进行预热,然后在 60 秒无操作后读取 /proc/<pid>/status 中的 VmRSS。每个版本执行三次:

  • Bun 1.3.14

- 第 1 次运行:预热后 RSS 34,876 KB,空闲 60 秒后 RSS 34,960 KB

- 第 2 次运行:预热后 RSS 34,924 KB,空闲 60 秒后 RSS 34,928 KB

- 第 3 次运行:预热后 RSS 34,872 KB,空闲 60 秒后 RSS 34,936 KB

  • Bun 1.4.2

- 第 1 次运行:预热后 RSS 18,864 KB,空闲 60 秒后 RSS 18,920 KB

- 第 2 次运行:预热后 RSS 19,068 KB,空闲 60 秒后 RSS 19,136 KB

- 第 3 次运行:预热后 RSS 19,056 KB,空闲 60 秒后 RSS 19,144 KB

空闲状态的平均值:从 34.9MB 降至 19.0MB,减少了 46%。发布说明声称“最高提升 35%”,因此在这个工作负载上,它超越了自身的数据。尽管一个纯粹的回显服务器非常接近这类声明的最佳情况——没有应用程序内存会掩盖运行时自身的占用。

空闲 CPU:这是声称未被验证的地方

发布说明声称空闲 CPU 减少了 5 倍。这是我将标记为“未被我的测量数据支持”的一项。

我分别在每个版本的 60 秒空闲窗口前后,从 /proc/<pid>/stat 采样 utime + stime,每个版本三次(系统时钟频率为每秒 100 个 tick,因此每个 tick 代表 10ms 的 CPU 时间):

  • Bun 1.3.14

- 第 1 次:11 ticks/60s

- 第 2 次:11 ticks/60s

- 第 3 次:10 ticks/60s

- 平均:10.67 ticks/60s

  • Bun 1.4.2

- 第 1 次:6 ticks/60s

- 第 2 次:4 ticks/60s

- 第 3 次:3 ticks/60s

- 平均:4.33 ticks/60s

这意味着 1.3.14 的平均 CPU 占用约为 0.178%,而 1.4.2 约为 0.072%——这是一个真实的改善,方向正确,但降幅是 2.5 倍,而非 5 倍。这两个数字都很小,单个坏样本就可能大幅改变比率,这也正是我从 5 秒窗口(总共只有 0 到 2 个 tick,毫无用处)改为 60 秒才开始采信的原因。Bun 的 5 倍数据可能来自不同的空闲工作负载——例如包含活动文件观察者或定时器循环的场景,而非一个没有任何流量的裸 Bun.serve——但在最简单的空闲服务器上,我测得的是 2.5 倍。

负载下的吞吐量

发布说明没有声称吞吐量数据,所以我还是测了一下,因为空闲数据只能告诉你服务器“无所事事”时的表现。我使用 autocannon -c 50 -d 8 对同一个回显服务器进行测试,每个版本运行三次:

  • Bun 1.3.14

- 第 1 次:29,456 平均请求/秒

- 第 2 次:29,578 平均请求/秒

- 第 3 次:28,042 平均请求/秒

- 平均:29,025 请求/秒

  • Bun 1.4.2

- 第 1 次:62,310 平均请求/秒

- 第 2 次:61,998 平均请求/秒

- 第 3 次:62,922 平均请求/秒

- 平均:62,410 请求/秒

在 50 个并发连接下,每秒请求数提升了 2.15 倍,并且 autocannon 输出中的 p50 延迟从 1 毫秒降至亚毫秒级。这个指标在并发情况下的表现比空闲时更好,这与我最初的预期恰恰相反——我原以为以空闲效率为目标的重写,应该在“无事发生”时展现最大收益,而非在负载之下。

回归问题:通过符号链接以node身份调用Bun会静默丢失.env文件

Bun 将自己定位为 Node 的直接替代品,一种常见的使用方式是将 node 二进制文件符号链接到 bun,这样现有工具无需更改即可运行。我测试的正是这种设置。

$ mkdir -p /tmp/nodebin && ln -s /root/.bun-old/bin/bun /tmp/nodebin/node
$ echo 'MY_SECRET=hello123' > .env
$ /tmp/nodebin/node check.js
MY_SECRET=hello123


这是 1.3.14 版本,通过一个名为 node 的符号链接调用。它按预期加载了 .env 文件。现在用 1.4.2 版本进行相同的设置:

$ mkdir -p /tmp/nodebin && ln -s /root/.bun-new/bin/bun /tmp/nodebin/node
$ /tmp/nodebin/node check.js
MY_SECRET=undefined


没有错误,没有警告,退出码为 0。进程只是以 MY_SECRET 未定义的状态运行。我检查了这是符号链接技巧特有的问题还是更广泛的回归:在 1.4.2 上,直接 bun run check.jsbun --bun run check.js 仍然能正确加载 .env。问题专门出在检测到“二进制文件名为 node”的代码路径上,这条路径不再自动加载环境文件。

这个问题很重要,因为“将 node 符号链接指向 Bun”是官方文档记载的迁移路径,并非我臆想的边缘情况。任何在容器镜像或 CI 流水线中以这种方式运行 Bun,并期望其行为兼容 Node 的用户,都会遇到一个进程干净启动却静默运行、配置缺失的情况。解决方案存在且有效:

$ /tmp/nodebin/node --env-file=.env check.js
MY_SECRET=hello123


但你必须知道需要添加它。正常的运行输出中没有任何信息提示你的 .env 被跳过了。

测试过程中我纠正的错误

在第一次基准测试后,我差点完全否定了关于启动速度的结论。bun --version 是一个快速路径,从不启动 JS 引擎,因此它并未测试发布说明中描述的功能,测试结果显示版本间几乎没有差异。只有在切换到一个实际会被解析和运行的脚本后,我才发现了真正的 60% 性能差距。如果第一次尝试得到“无差异”结果,值得反思一下:你测量的是功能本身,还是仅仅测量了二进制文件的退出路径。

空闲 CPU 的情况也是如此:我最初的采样窗口是 5 秒,每次运行总共只产生 0-2 个时钟周期——分辨率不足以得出任何结论。将窗口延长到 60 秒后,数据才变得可用,也正是在这时暴露出所谓的“5 倍提升”在这个测试规模下并不成立。

自行测试

# 并行安装两个版本
BUN_INSTALL=~/.bun-old bash -c 'curl -fsSL https://bun.sh/install | bash -s "bun-v1.3.14"'
BUN_INSTALL=~/.bun-new bash -c 'curl -fsSL https://bun.sh/install | bash'

# 启动时间测试
echo 'console.log("hi")' > hello.js
python3 -c "
import subprocess, time, statistics
for label, binpath in [('old', '$HOME/.bun-old/bin/bun'), ('new', '$HOME/.bun-new/bin/bun')]:
    times = []
    for _ in range(30):
        t0 = time.perf_counter()
        subprocess.run([binpath, 'run', 'hello.js'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
        times.append((time.perf_counter() - t0) * 1000)
    times.sort()
    print(label, 'median', round(times[15], 2), 'ms')
"

# 测试node符号链接导致的.env加载回归问题
mkdir -p /tmp/nodebin && ln -sf ~/.bun-new/bin/bun /tmp/nodebin/node
echo 'MY_SECRET=hello123' > .env
echo 'console.log("MY_SECRET=" + process.env.MY_SECRET)' > check.js
/tmp/nodebin/node check.js


如果你通过任何将Bun呈现为node的方式(例如容器基础镜像、nvm风格的垫片、将运行时符号链接的CI运行器)运行Bun 1.4,在依赖它之前请务必检查一件事:在你的.env中放入一个临时变量,通过那个符号链接运行你的实际入口点,然后将变量打印出来。如果它返回undefined,那么你就发现了与我相同的问题,在启动命令中添加--env-file就是解决办法。无论如何,启动速度和内存占用的提升都值得拥有;我宁愿在这里发现配置缺失的问题,而不是在收到支持工单时才发现。

原文:https://dev.to/alexgeorgiev17/bun-14s-rust-rewrite-cuts-script-startup-time-from-105ms-to-42ms-2je0(作者 @alexgeorgiev17)

发布评论
全部评论(0)