Bun 如何将 postMessage(string) 提速 500 倍,远超 NodeJs 🚀🚀🚀

在 JavaScript 中,postMessage 是在多个工作线程之间传递数据的常用方法。在 Bun v1.2.21 中,postMessage(string) 的性能几乎与字符串大小无关。这对于多线程 JavaScript 服务器和命令行工具是一个巨大的改进。

通过避免对已知安全共享的字符串进行序列化,性能提高了多达 500 倍,并且在此基准测试中使用的峰值内存减少了约 22 倍。

字符串大小 Bun 1.2.21 Bun 1.2.20 Node 24.6.0
11 字符 543 ns 598 ns 806 ns
14 KB 460 ns 1,350 ns 1,220 ns
3 MB 593 ns 326,290 ns 242,110 ns

这种优化在你将字符串发送到工作线程时自动生效:

javascript 复制代码
const response = await fetch("https://api.example.com/data");
const json = await response.text();

postMessage(json); // 对于大字符串,现在速度提升了 500 倍

这对于在工作线程之间传递大型 JSON 数据的应用程序特别有用,例如 API 服务器、数据处理管道和实时应用程序。

如下代码所示:

ts 复制代码
async function measureTime() {
  const url = "https://microsoftedge.github.io/Demos/json-dummy-data/5MB.json";

  // 开始计时
  console.time("Total Time");

  try {
    const response = await fetch(url);

    if (!response.ok) {
      throw new Error(`HTTP Error! Status: ${response.status}`);
    }

    // 等待JSON解析完成,但不将其赋值给变量,因为我们不需要使用它
    await response.json();

    console.timeEnd("Total Time");
  } catch (error) {
    // 如果发生错误,则结束计时并打印错误信息
    console.timeEnd("Total Time");
    console.error("An error occurred:", error);
  }
}

measureTime();

首先我们分别使用 NodeJs 和 Bun 的旧版本分别测试一下,最终输出结果如下图所示:

当我们把版本升级到最新之后,速度明显增高:

技术揭秘:JavaScriptCore 的优化

postMessage 通常使用结构化克隆算法(Structured Clone Algorithm)在发送到另一个线程之前序列化数据。这意味着将字符串的每个字节复制到一个新缓冲区,然后在另一端反序列化。

但问题是:在 JavaScriptCore(Bun 使用的引擎)中,字符串已经是线程安全的引用计数对象。字符串数据在创建后是不可变的,引用计数使用 std::atomic

cpp 复制代码
class StringImplShape {
    std::atomic<unsigned> m_refCount;  // 线程安全!
    unsigned m_length;                  // 不可变
    union {
        const LChar* m_data8;           // 不可变
        const char16_t* m_data16;       // 不可变
    };
    mutable unsigned m_hashAndFlags;    // 唯一可变部分
};

因此,如果字符串已经是线程安全的,为什么在同一进程中的线程之间发送时还要进行序列化呢?([bun.com][1])

寻找快速路径

并非所有字符串都可以安全地共享。我们识别出三种需要序列化的类型:

  1. 原子字符串(Atom strings):线程本地的属性名和符号。

  2. 子字符串(Substrings):指向其他具有复杂生命周期的字符串。

  3. 绳状字符串(Rope strings):通过操作如 "foo" + "bar".slice() 创建的字符串。

对于其他所有字符串,我们可以完全跳过序列化。我们只需要确保在共享之前计算出惰性计算的哈希值(因为这是唯一可变的部分):

cpp 复制代码
WTF::String toCrossThreadShareable(WTF::String& string)
{
    auto* impl = string.impl();

    // 不能共享原子、符号或子字符串
    if (impl->isAtom() || impl->isSymbol() ||
        impl->bufferOwnership() == StringImpl::BufferSubstring)
        return string.isolatedCopy();

    // 在共享之前强制计算哈希
    impl->hash();

    // 防止该线程进行原子化。
    impl->setNeverAtomicize();

    return string;  // 直接共享指针!
}

toCrossThreadShareable 函数通过识别哪些字符串可以安全地共享而无需序列化,实现了性能的显著提升。这种优化在多线程 JavaScript 服务器和命令行工具中尤为重要。理解其背后的原理有助于我们在开发中更高效地处理字符串数据。

快速路径条件

当满足以下条件时,优化将生效:

  • 你正在使用 postMessagestructuredClone

  • 你只发送一个字符串(而不是混合数据)。

  • 字符串不是子字符串、绳状字符串、原子或符号。

  • 你将字符串发送到同一进程中的另一个线程。

  • 字符串长度 ≥ 256 字符。

这涵盖了在工作线程之间发送字符串的极其常见的模式。

总结

通过识别哪些字符串可以安全地共享而无需序列化,Bun 在 postMessage(string) 上实现了显著的性能提升。这种优化对于多线程 JavaScript 服务器和命令行工具尤其有用。

相关推荐
冰暮流星1 分钟前
javascript之数组
java·前端·javascript
晚霞的不甘29 分钟前
Flutter for OpenHarmony天气卡片应用:用枚举与动画打造沉浸式多城市天气浏览体验
前端·flutter·云原生·前端框架
xkxnq42 分钟前
第五阶段:Vue3核心深度深挖(第74天)(Vue3计算属性进阶)
前端·javascript·vue.js
三小河1 小时前
Agent Skill与Rules的区别——以Cursor为例
前端·javascript·后端
Hilaku1 小时前
不要在简历上写精通 Vue3?来自面试官的真实劝退
前端·javascript·vue.js
三小河1 小时前
前端视角详解 Agent Skill
前端·javascript·后端
Aniugel1 小时前
单点登录(SSO)系统
前端
颜酱1 小时前
二叉树遍历思维实战
javascript·后端·算法
鹏多多1 小时前
移动端H5项目,还需要react-fastclick解决300ms点击延迟吗?
前端·javascript·react.js
serioyaoyao1 小时前
上万级文件一起可视化,怎么办?答案是基于 ParaView 的远程可视化
前端