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.com1)

寻找快速路径

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

  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 服务器和命令行工具尤其有用。

相关推荐
新中地GIS开发老师22 分钟前
WebGIS开发学生作品|低空航天管理与航线规划系统
前端·javascript·webgis·三维gis开发
用户0595401744642 分钟前
大模型对话记忆持久化踩坑实录:用 Playwright 自动化测了 300 次,终于揪出会话丢失的真凶
前端·css
丙氨酸長鏈44 分钟前
Web前端入门第 问:JavaScript 一个简单的 IndexedDB 数据库入门示例
前端·javascript·数据库
kyriewen2 小时前
面试官让我手写虚拟列表——AI生成的版本,快速滚动几下就白屏了
前端·javascript·面试
IT_陈寒2 小时前
Vite热更新失效?你可能漏了这个配置
前端·人工智能·后端
梅孔立3 小时前
两种免费的翻译API调用方式详解 - Edge官方API与个人缓存服务
前端·缓存·edge
Revolution613 小时前
React 组件重新渲染时,到底重新执行了什么
前端·react.js·面试
用户2181697049304 小时前
Flutter(四)Dart语法 空安全 运算符 流程控制
前端
许彰午4 小时前
政务督办的分合模式:主办协办的并发审批
前端·javascript·政务
用户69371750013844 小时前
AI时代,程序员该往哪走?
前端·后端