Google V8:我们如何让 JSON.stringify 的速度提升超过两倍
JSON.stringify
是 JavaScript 用于序列化数据的核心函数。其性能直接影响网络请求中的数据序列化、将数据保存到 localStorage
等常见操作。更快的 JSON.stringify
意味着更快的页面交互和更响应的应用程序。这就是为什么我们很高兴分享最近的工程努力使 V8 中的 JSON.stringify
速度提升了 超过两倍。本文将详细解析实现这一改进的技术优化。
无副作用的快速路径
此次优化的基础是一个基于简单假设的新快速路径:如果能保证序列化对象不会触发任何副作用,即可使用更快速的专用实现。在此上下文中,“副作用”指任何会破坏对象简单、流畅遍历过程的操作。
这不仅包括显而易见的场景(如序列化过程中执行用户定义代码),还包括可能触发垃圾回收循环的更微妙的内部操作。有关具体哪些操作会引发副作用以及如何避免它们的详细信息,请参阅限制。

只要 V8 能确定序列化过程不会受到这些副作用的影响,它就可以继续使用这条高度优化的路径。这使得它能够绕过通用序列化器所需的许多昂贵检查和防御性逻辑,从而显著提升常见类型 JavaScript 对象(代表纯数据)的序列化速度。
此外,新的快速路径是迭代的,而通用序列化器则是递归的。这种架构选择不仅消除了对栈溢出检查的需求,并允许我们在编码更改后快速恢复,还使开发人员能够序列化比以前更深的嵌套对象图。
处理不同的字符串表示形式
V8 中的字符串可以使用单字节或双字节字符进行表示。如果字符串仅包含 ASCII 字符,它们将以单字节字符串的形式存储在 V8 中,每个字符占用 1 个字节。然而,如果字符串中包含任何一个超出 ASCII 范围的字符,该字符串中的所有字符都将使用双字节表示形式,这会使内存占用量翻倍。
为避免统一实现中频繁的分支和类型检查,整个字符串化器现已基于字符类型进行模板化。这意味着我们编译了两个独立的专用版本的序列化器:一个完全优化用于单字节字符串,另一个用于双字节字符串。这会影响二进制文件大小,但我们认为性能提升绝对值得。
该实现可高效处理混合编码。在序列化过程中,我们必须先检查每个字符串的实例类型,以识别无法在快速路径中处理的表示形式(如ConsString
,这可能在扁平化过程中触发垃圾回收),这些情况需要切换到慢速路径。这一必要的检查同时揭示了字符串使用的是单字节还是双字节编码。
因此,从乐观的一字节字符串转换器切换到双字节版本的决策本质上是免费的。当现有检查发现双字节字符串时,会创建一个新的双字节字符串转换器,继承当前状态。最终结果通过简单地将初始一字节字符串转换器的输出与双字节字符串转换器的输出拼接而成。此策略确保我们在常见情况下保持高度优化的路径,同时切换到处理双字节字符的过程轻量且高效。
通过SIMD优化字符串序列化
JavaScript中的任何字符串都可能包含在序列化为JSON时需要转义的字符(例如"
或\
)。传统的按字符循环查找这些字符的方法效率低下。
为加速此过程,我们采用基于字符串长度的两级策略:
- 对于较长字符串,我们切换至专用硬件SIMD指令(如ARM64 Neon)。这允许我们将字符串的大部分内容加载到宽SIMD寄存器中,并通过少量指令一次性检查多个字节中的可转义字符。(source)
- 对于较短的字符串,由于硬件指令的设置成本过高,我们采用了一种名为 SWAR(SIMD Within A Register)的技术。该方法利用标准通用寄存器上的巧妙位运算逻辑,以极低的开销同时处理多个字符。(source)
无论采用何种方法,该过程均高度高效:我们快速按块扫描字符串。若某块不包含任何特殊字符(常见情况),可直接复制整个字符串。
快速路径中的“快速通道”
即使在主快速路径中,我们也发现了一个更快的“快速通道”机会。默认情况下,快速路径仍需遍历对象的属性,并对每个键执行一系列检查:确认键不是 Symbol
,确保其可枚举,最后扫描字符串中需要转义的字符(如 "
或 \
)。
为消除这一步骤,我们在对象的隐藏类上引入了一个标志。当我们完成对象所有属性的序列化后,若所有属性键均非Symbol
、所有属性均可枚举且无属性键包含需转义的字符,则将该对象的隐藏类标记为fast-json-iterable
。
当序列化一个与之前序列化的对象具有相同隐藏类的对象(这很常见,例如一个包含形状相同的对象数组)且该对象为 fast-json-iterable 时,我们可以直接将所有键复制到字符串缓冲区,无需进一步检查。
我们还为 JSON.parse
添加了此优化,可在解析数组时利用其进行快速键值比较,前提是数组中的对象通常具有相同的隐藏类。
更快的双精度浮点数转字符串算法
将数字转换为字符串表示形式是一项出人意料的复杂且性能关键的任务。在对 JSON.stringify
的优化工作中,我们发现通过升级核心 DoubleToString
算法,可以显著提升该过程的效率。我们已用 Dragonbox 替换了长期使用的 Grisu3 算法,用于最短长度数字到字符串的转换。
尽管这一优化是由 JSON.stringify
的性能分析驱动的,但新的 Dragonbox 实现将惠及 V8 中所有对 Number.prototype.toString()
的调用。这意味着任何将数字转换为字符串的代码,而不仅仅是 JSON 序列化,都将免费获得这一性能提升。
优化底层临时缓冲区
在任何字符串构建操作中,内存管理是主要开销来源。此前,我们的字符串化器在 C++ 堆上使用单个连续缓冲区构建输出。虽然简单,但这种方法存在显著缺点:每当缓冲区空间不足时,我们必须分配更大缓冲区并复制现有内容。对于大型 JSON 对象,这种重新分配和复制的循环会造成重大性能开销。
关键的洞察是,强制要求临时缓冲区连续并无实际意义,因为最终结果仅在最后一刻才被组装成单个字符串。
基于此,我们用分段缓冲区替换了旧系统。现在,我们不再使用一个不断增长的大型内存块,而是使用一个由较小缓冲区(或“分段”)组成的列表,这些分段在V8的Zone内存中分配。当一个分段已满时,我们只需分配一个新的分段并继续写入其中,完全消除了昂贵的复制操作。
限制
新的快速路径通过专门针对常见的简单情况来实现其速度。如果要序列化的数据不符合这些条件,V8 将回退到通用序列化器以确保正确性。要获得完整的性能优势,JSON.stringify
调用必须满足以下条件。
- 不提供
replacer
或space
参数:提供replacer
函数或space
/gap
参数用于格式化输出是通用路径专属的功能。快速路径专为紧凑、未转换的序列化设计。 - 普通数据对象和数组:被序列化的对象应为简单的数据容器。这意味着它们及其原型不得包含自定义的
.toJSON()
方法。快速路径假设使用标准原型(如Object.prototype
或Array.prototype
),且这些原型不包含自定义序列化逻辑。 - 对象中不包含索引属性:快速路径针对具有常规字符串键的对象进行了优化。如果对象包含类似数组的索引属性(例如
‘0’, ‘1’, ...
),则将由较慢的通用序列化器处理。 - 简单字符串类型:某些内部 V8 字符串表示形式(如
ConsString
)在序列化前可能需要先进行内存分配以实现扁平化。快速路径会避免任何可能触发此类分配的操作,因此最适合处理简单、顺序的字符串。作为网页开发者,这部分内容较难直接影响。但请放心,在大多数情况下它应能正常工作。
对于绝大多数用例,例如序列化 API 响应数据或缓存配置对象,这些条件自然满足,开发者可自动享受性能提升。
结论
通过从头重新设计 JSON.stringify
,从其高级逻辑到核心内存和字符处理操作,我们在 JetStream2 json-stringify-inspector 基准测试中实现了超过 2 倍的性能提升。请参见下图中不同平台的测试结果。这些优化自 V8 版本 13.8(Chrome 138)起可用。
本文文字及图片出自 How we made JSON.stringify more than twice as fast
JSON编码是NodeJS中进程间通信的巨大障碍。
迟早有一天,似乎每个人都会想到通过将事件循环的阻塞任务转移到另一个线程来减少NodeJS代码中的事件循环阻塞,结果却发现主线程的CPU负载翻了三倍。
我见过有人逐个元素将数组转换为字符串。听起来他们现在可能在内部这样做了。
如果可以的话,我建议 V8 团队在这方面走得更远。能否避免为数据子集退出?CString 问题如何解决?这是否能让 faststr 死而复生?
基于我去年首次尝试节点性能分析的经验,JSON.stringify 是影响高性能节点服务几乎所有方面的最大障碍。所有人都使用 stringify 处理字典键,Apollo/Express 直接将整个响应序列化为字符串而非增量流式传输(我认为可能有解决方法,但看起来很 hacky)
作为一名来自 JVM/Go 背景的开发者,我不得不说,这种情况让我感到有些震惊。
> JSON.stringify 是影响高性能 Node 服务几乎所有方面的最大障碍
我也有同样的体验。但我认为更深层的问题在于 Node 的协作式多任务模型。如果采用抢占式多任务(如 Go),在序列化大型响应时(GraphQL 场景常见,其他 API 也可能遇到)就不会阻塞整个事件循环(其他并发任务)。是的,确实感觉很业余。
这其实不是 Node 的问题,而是 JavaScript 的问题。它本身并未设计成支持像 Go 等语言那样的并行执行。这就是为什么他们会使用 Web Workers、独立进程等来利用多个核心。但这样做后,你可能仍需依赖 JSON 序列化在不同事件循环间传输数据。
我不是 Node 开发者,但我在网页上做过一些 JavaScript 工作。为什么他们要在 v8 隔离区之间使用 JSON 发送数据,而 sendMessage 允许发送整个对象(底层使用一种实现定义的二进制序列化协议,速度快 5-10 倍)?
最大的原因很可能是他们不理解序列化,只是认为对象以某种方式从一个工作者发送到另一个工作者,这听起来像是一个简单的引用操作。
> 这不是 Node 的问题,而是 JavaScript 的问题。
如今 Node 就是 JavaScript。他们是过去十年左右推动 JavaScript 标准和新功能发展的核心力量。没有什么能阻止他们逐步开始添加真正的并行处理、多线程等功能。
我不同意——JavaScript 是全球使用最广泛的编程语言,如果我是个赌徒,我会毫不犹豫地押注客户端 JavaScript 仍占更大份额。
是的,现代 JavaScript 在原子操作和其他有趣的内存功能方面取得了许多进展,但这只是像 JavaScript 这样流行语言的自然演进。三大 JavaScript 引擎仍主要针对网页浏览器开发,而网页开发者是 ES 语言讨论中主要考虑的受众(尽管我承认近年来服务器运行时已被越来越多的考虑)
>我不同意——JavaScript 是全球使用最广泛的编程语言,若我是赌徒,我乐意打赌客户端 JavaScript 仍占其中绝大多数份额,而非服务器端。
客户端仍以 V8 为主,占比约 90%,与 Node.js 几乎无异。
“没有什么能阻止他们逐步添加真正的并行处理、多线程等功能。”
从理论上讲或许可行。但从实践经验来看,试图将这些功能强加到一个使用了几十年的单线程脚本语言上,是一个极其困难且容易出错的过程,可能需要十年时间才能达到生产质量,如果真的能做到的话,然后再需要十年或更长时间才能成为一个可以预期正常工作、预期找到正确使用的库等的东西。
我认为这并非脚本语言的固有特性。如果从头开始设计一种语言,它在多线程方面可能不会比其他语言遇到更多问题。但试图将多线程功能添加到已经单线程运行了十几年甚至更久的语言中,则非常、非常困难。坦白说,考虑到其他语言在这一领域的尝试结果,我认为有必要与开发团队进行非常、非常、非常认真的讨论,以确定这是否真的值得。其他脚本语言在这一领域投入了大量工作,但据我观察,成果并未达到投入的价值。
> 是的,这确实有点像业余水平。
Node.js 专为 I/O 密集型工作负载设计。具体来说,它适用于那些 不适合在 CPU 上进行并行处理 的工作负载。
这是因为 JavaScript 是一种严格的单线程语言;即,它不支持多个线程对内存的共享访问。(而这是因为 JavaScript 最初是为控制用户界面而设计的,而历史上用户界面始终是在单线程上处理的。)
如果你需要真正的多线程处理,有很多语言支持这一功能。要么你选择了错误的语言,要么你可能需要考虑用另一种语言创建一个库,然后从 NodeJS 中调用它。
> 基于我去年首次进行的 Node 性能分析,JSON.stringify 是影响高性能 Node 服务性能的最大障碍之一
确实如此。它,或者至少可以是,Nodejs中阿姆达尔定律计算中顺序部分的主要因素。
我很好奇,本文中关于“无副作用”的评论是否涉及将JSON计算的一部分移出事件循环。如果属实,这将非常有趣。
然而,出于并发考虑,我怀疑它可能无法完全移出。你可能能做到的最好方式是让多个线程在事件循环被阻塞时转换对象。这与JVM中的并发标记机制有些类似。
Node是高性能Node服务最大的障碍。其核心价值主张是“如果你能雇佣那些使用世界上最流行编程语言写代码的人,会怎样?” 猜猜看
我认为核心价值在于你可以共享代码(以及使用 TypeScript 时共享类型)在你的 Web 前端和后端之间。
这很有用,但如果你能使用 OpenAPI 规范你的 API,然后生成 TypeScript API 客户端,你也可以实现类似的优势。许多 Web 框架都使从代码生成 OpenAPI 规范变得容易。
维护负担从手动同步类型,转移到设置和维护往往相当复杂的代码生成步骤。一旦配置好并运行顺畅,这是一个不错的系统,在我看来通常是值得的。
最大的好处不是在创建新类型时提高生产力,而是整体可靠性和修改现有内容的便捷性。
完全同意。
我从事这项工作已久,从未“在前端和后端之间共享代码”,但跨语言共享类型才是关键。
在该评论树的另一条评论中,我提到使用 TypeScript 还能做得更好。如果你能从后端导入类型,就不需要代码生成。OpenAPI 没问题,但我真的很讨厌这种中间层。
只需将 API 接口定义为从 API 路由函数定义中提取类型的集合。让 API 函数从模型层提取类型。将这些类型转换为 JSON 反序列化后的形式,现在你就可以将数据库中的模式直接传递到客户端。无需编译客户端。无需运行监听器。它始终保持同步且评估速度快。
我发现这在实际中并没有我希望的那么有效……你呢?
我也不觉得,原因有很多,尤其是如果你在打包前端代码。很容易不小心包含与浏览器不兼容的代码,这变成了一场猫捉老鼠的游戏。
关于类型,我认为真正的价值在于拥有所有领域类型的单一数据源,但由于中间存在序列化层(HTTP),情况很少如此简单。我已经退回到在需要的地方显式地为前端进行类型定义,这样简单得多,而且工作量也不大。
(基本上,一旦你有了任何类型的上下文特定序列化,比如排除或转换字段,或者你在API中为关系提供了“预填充”选项等——你最终会在后端和前端之间编写类型映射代码,而这种代码往往会很快变得脆弱)
这对于特定场景非常有用:统一且复杂的领域逻辑,其运行速度比往返服务器更快。
我很少需要这样做。我记忆中两个例子是:首先是事件和日历逻辑,其次是实现封装WebRTC的协议。
是的,使用前端和后端单一语言带来了惊人的生产力提升。
这就是Java的故事。
一种语言统治一切:在服务器上、在客户端、在浏览器中、在设备中。它确实曾经无处不在。
然后人们开始渴望更好的解决方案,并转向专用语言。
换句话说,对于大多数开发团队而言,使用单一语言带来的生产力提升远非惊人,甚至在典型场景下会带来负面影响。
Java小程序从未像JavaScript在网页上那样无处不在——事实上,每个网页上都始终存在一个JavaScript运行环境,除非用户明确禁用它,而这样做的人少之又少。
JavaScript 将继续作为网页的主要脚本语言,这意味着 Node.js 作为后端脚本语言可能会有其一席之地。许多后端系统都是相对简单的 CRUD API 应用,使用 Node.js 完全可行,且能够在前端和后端之间共享类型定义等内容确实存在实际优势。
> 能够在前端和后端之间共享类型定义等确实有实际好处
虽然有好处,但也有缺点。如你所指出的,如果后端只是简单地代理数据库,任何语言都可以,所以你可能不如使用与前端相同的语言。
我认为,运营了几年的公司中,很少有后端仍然如此简单。到了某个阶段,你将希望向前端隐藏或抽象化某些内容。后端将承担越来越多的处理任务、验证工作,并处理越来越多的领域特定逻辑(如税务/货币、审计、调度等)。它逐渐演变为一个独立的复杂系统,你不会继续使用一种仅因能与前端部分共享类型而存在的语言。
Java在桌面或浏览器上运行得并不理想(可以说它从未真正能在浏览器上运行),而且在那个时代,它总体上是一种生产力极低的语言。
在所有地方使用单一语言确实有显著优势。虽然不足以完全压倒其他所有方案——使用两种好语言仍能胜过一种差语言——但在其他条件相同的情况下,使用单一语言的表现会好得多。
是的,Java 从来都不是真正优秀的(我认为在任何平台上都是如此。服务器端还行,但在我看来不算“真正优秀”)
这让我思考了为使 JavaScript 成为今日的强大语言所付出的巨大努力。
即使在浏览器中,我们能够实现这些疯狂的功能,也是因为谷歌、苹果和火狐付出了巨大的努力,对每个角落进行优化,并构建了与运行它们的操作系统复杂度相当的运行时环境,以至于我们得到了Chrome OS作为副产品。
从这个角度来看,我们或许可以选择任何一门语言,投入同样的努力,使其成为性能卓越的平台。Java本可以做到这一点,如果我们真的足够努力的话。只是除了Sun和Oracle之外,其他大型厂商都没有这样的动力。
> 在其他条件相同的情况下,使用单一语言会表现得更好。
是的,确实存在某些特定场景下专用服务器堆栈反而成为负担的情况。但坦白说,我尚未发现太多此类案例。在极端情况下,人们会转向Firebase等平台,并投入资金完全抽象化服务器端。
这对于在客户端和服务器端运行Zod验证器非常有用,因为你可以实现实时验证,而无需向服务器发送请求。
我利用将后端类型导入前端的功能,实现了一个零成本、无需文件监听器的API验证器。
我的博客文章这里不够好,但希望它能传达要点
https://danangell.com/blog/posts/type-level-api-client/
但这仅在最后一层(后端为前端模式)中真正相关;随着组织或领域扩展,可添加更多层级。例如,我当前工作中,后端是一个长期运行的SAP系统。TypeScript API层在每个参数上堆积了大量注解和杂项,这意味着其直接用于前端时实用性较低。相反,会基于 TS / 注释生成一个 OpenAPI 规范,然后使用该规范生成 API 客户端或前端/简化 TS 类型。
简而言之,这种价值主张是有限的。
Node.js 永远不会像 VB 那样糟糕。
我知道 JavaScript 很快,但…
我无法接受如此混乱的生态系统。对于流量较低或中等的后端代码,我更倾向于使用 Python。
我知道 Python 在部署方面并不出色,但这门语言非常易于理解,我拥有适用于各种场景的工具,可以轻松从 C++ 代码中提供绑定,而且使用起来非常愉快。
如果它继续提升性能,我认为我会继续使用它处理大量后端任务(除了高性能场景,最近我主要用C++和Capnproto RPC处理分布式任务)。
这归结于语言偏好。我认为Python是计算机科学领域最糟糕的语言。函数中没有嵌套作用域,由于缺乏作用域,变量会在分支和循环中泄漏,没有经典的for循环,但最糟糕的是,安装Python包和框架从未顺利过。
我本想喜欢 Jupyter 笔记本,因为笔记本非常适合原型设计,但 Jupyter 和 Python 绘图库太笨拙且速度太慢,我总是不得不退而求其次,使用 Node 或用 JS 和 SVG 编写网页进行绘图和原型设计。
这完全取决于你衡量的标准:
– 快速应用程序开发
VB更简单快捷
– 图形用户界面开发
至少在Windows上,依我之见,VB仍是为此目的创建的最佳语言。Borland的IDE曾尝试改进,但没有任何东西能与VB6在速度和开发便捷性上相媲美。
当然这并非 JavaScript 的问题,但与之相比,CSS 等技术确实显得杂乱无章。
– 跨平台开发
你的观点有道理。不过 VB6 属于不同时代。
– 类型安全
VB6 在此再次胜出
– 可读性
这确实是主观的,但我个人认为典型的 Node.js 代码并不算特别易读。VB 受 ALGOL 启发的设计并非人人都能接受,但我个人并不介意使用 Begin/End 代码块。
– 一致性
JS 有太多奇怪的边界情况。这并非说 VB 没有自己的怪癖,但在我的经验中,它们的数量要少得多。
不同JS实现之间也存在不一致性。
– 并发性
两种语言在这方面都表现不佳。虽然Node.js有async/await,但我个人讨厌这种设计,而且最终Node.js的核心仍然是单线程的。因此,尽管JS在技术上更优,但它仍然糟糕到无法让我在这里给它胜利。
– 开发者熟悉度
JS的使用者更多。
– 代码持久性
考虑到 JavaScript 框架更迭的已知问题,这个指标是否值得反驳?你甚至无法重新编译任何规模较大的两年旧 JavaScript 项目而不遇到问题。几乎所有其他流行语言在这一点上都优于 JavaScript。
– 开发工具
VB6 随附了你所需的一切,并在完成 VB Visual Studio 安装后即可正常工作。
而使用 Node.js 时,你需要手动配置大量不同的组件才能开始使用。
—
我并不是建议人们用 VB 编写新软件。但它确实是一款适合其设计目的的好语言。
Node/JS 甚至不是一款适合其设计目的的好语言。它只是一个混乱的生态系统。就连维护核心 JS 组件的人都清楚这一点——这就是为什么工具链不断迁移到其他语言如 Rust 和 Go。而许多人围绕其定制的 JS 运行时创建业务,旨在解决 Node.js 带来的问题(从而因不断增加的“标准”数量而制造更多问题)。
Node.js 唯一可取之处在于其网络效应,即所有人都使用它。但对我来说,这更像是斯德哥尔摩综合症,而非发自内心的赞誉。
如果对 Node.js 的最高评价是“它比那个20年前就已消亡的生态系统更好”,那么你自己也该明白事情有多糟糕。
> 但对我来说,这更像是斯德哥尔摩综合症
顺便提一下,斯德哥尔摩综合症并不存在。不过我基本上同意你的观点,人们就是喜欢自己熟悉的东西,而且可能对最早学习或使用最久的事物有偏好。
我几乎是无意中将一个简单的 Excel 电子表格变成了一个相当大的数据管理系统。一旦你弄清楚了瓶颈所在,你会惊讶于 VB 如今的速度有多快。
情况已经更糟了。尽管VB作为一门语言存在诸多不足,但它作为开发环境的效率却是惊人的。
标准太低了!
> 如果有什么建议,我会鼓励V8团队在这方面走得更远。
这似乎不是正确的方向。我建议遇到这个问题的人另寻他法。Node/V8并不适合后端或更复杂的计算问题。JavaScript是由网页使用场景塑造的,这种情况在短期内不会改变。你不能指望V8团队来拯救他们。
TypeScript团队转向Go语言,因为Go与TS/JS足够相似,可以自动完成部分翻译。我不是AI爱好者,但它们在进行符合语言习惯的翻译方面也相当出色。
> Node/V8并不适合后端
Node从设计之初就旨在做好一件事——后端网络服务开发。
它在这方面表现得异常出色。与JVM相比,其运行时开销微乎其微,异步模型简单到令人难以置信,且复杂度仅为其他语言在该领域实现方案的冰山一角。即使在性能低下的CPU上运行,Node也能轻松处理每秒数千次请求,即使使用最简单的代码也游刃有余。
此外,该语言的紧凑性令人惊叹,你可以用不到十几行代码就搭建好一个完整的 ExpressJS 服务,包括身份验证功能。其中发生的“魔法”几乎为零,尤其与其他语言和框架相比。我知道有些人喜欢那种“魔法”功能(FastAPI 的一些实现确实很巧妙),但 Express 默认就是“所见即所得”。
> Typescript 团队转向 Go,因为它与 TS/JS 足够相似,可以自动完成部分翻译。
TS 团队转向 Go,因为 JS 在处理字符串或双精度数以外的任何事情时都表现糟糕。缺乏整数类型阻碍了语言的发展,因此运行时需要做大量工作来尝试确定何时可以将一个数字视为整数。
JS 的类型系统既异常灵活又具有局限性。由于 JS 基本上允许你对类型进行任何操作,TypeScript 因此成为最强大的类型系统之一,并实现了大规模采用。(是的,其他语言拥有更强大的类型系统,但没有一种语言能像 TS 那样实现广泛采用。)
如果我需要建模一个问题领域,TypeScript 是一个优秀的工具。如果我需要处理成千上万个小型请求,Node.js 是一个优秀的工具。如果我需要对这些传入的请求进行实际计算,嗯,也许应该选择另一个技术栈。
但对于大多数由“从用户获取消息、查询数据库、重新格式化数据库响应、发送给用户”组成的服务端点,Node.js 在解决这个问题上堪称卓越。
> Node 实际上就是为了一件事而设计的——后端网络服务开发。
我认为并非如此,至少最初不是这样。即使是这样,这也不意味着它实际上很好,更不用说适用于所有情况了。
> 即使在性能低下的 CPU 上运行,Node 也能轻松处理每秒数千次请求,即使使用最简单的代码。
父评论正是指这一点。它在某个临界点会崩溃。
> 你可以用不到十几行代码就搭建好一个完整的 ExpressJS 服务,包括身份验证。
易用性在初期确实不错,但通常会演变成技术债务。例如,你可以编写一个相当简洁的搜索算法,但它的性能会非常糟糕。初期这并不是问题。你可以使用任何主流语言和框架,用少量代码搭建一个服务。甚至有无需编写代码的服务器。但随着应用程序的增长,你将不得不添加越来越多的权宜之计。天下没有免费的午餐。
> TS团队转向Go是因为JS在处理字符串或双精度数值以外的任务时表现糟糕。
他们转向Go是因为V8引擎运行速度过慢且占用大量内存。至少,这是他们所写的。但这并非我想要讨论的重点。我试图说明的是,如果你必须更换语言,Go是一个不错的选择,因为它与JS/TS非常接近。
> 但对于大多数服务端点……
因为它们很简单,正如你所说。但当遇到问题时,让V8团队通过更多黑客手段来解决问题似乎并不妥当。
Python也存在相同问题。如果有高效的IPC原语,并在此基础上提供更高层次的API来处理常见模式,那将非常理想。
你的意思是:
> JSON编码是通信的巨大障碍
我好奇在全球范围内,JSON编码会给通信带来多少计算开销,相比直接发送固定格式的字节或更高效的解析格式(如ASN.1)而言。
不。因为痛苦的代码永远不会像较不痛苦的代码那样被优化。人们会说服自己去寻找其他解决方案,而片面的视角会导致局部最优解。
顺便说一句,我经常用“我并不是在优化方面更出色,而是更固执”来概括这一点。
我有多少次是抱着优化10%的目标开始,结果稍微退一步重新整理代码,反而找到了25%的优化空间、更好的可维护性,以及为市场部抱怨了三年但开发团队一直坚持无法在合理时间内实现的功能留出空间?平均下来,每家雇主至少有三次这样的奇迹,这已经是个相当不错的数字了。
是的。我认为我只遇到过一种情况,将工作卸载到 worker 节省的时间比序列化/反序列化所耗费的时间更多。进行大量计算通常意味着处理海量数据集——这意味着通过消息传递传递数据的成本会随着并行化工作的收益而增加。
我认为这些线索都可以在MDN文档的Web Workers部分找到。让一个Worker充当服务的前置代理;你向它发送一个URL,它会判断是否需要发起网络请求,然后为你处理响应并发送压缩后的结果。
大多数任务在中间阶段的内存占用比开始和结束时更高。如果进程之间只能通过设置字节来通信,那么开始和结束时的内存占用代表了通信开销,即延迟。
但这也是为什么像 p-limit 这样的机制有效——它们在归纳阶段暂停一组任意任务,避免数据在内存中扩展为需要与所有同行并发保留的复杂状态。通过部分线性化,你可以限制峰值内存使用,而Promise.all(arr.map(…))无法做到这一点,不仅仅是解决“洪水效应”的问题。
这对JVM性能也是一个重大问题。JSON编码本质上是一项耗费资源的操作。
对于JVM性能提升,希望在Node.js领域也能实现的一点是,JSON序列化库能够流式输出序列化结果。JSON的主要成本在于内存占用。字符串在内存中占用的空间远大于普通对象。
由于JVM通常仅将JSON用作通信协议,流式输出具有很大意义。I/O 操作(通常)耗时足够长,既能为 CPU 提供喘息机会,又能节省内存。
> 似乎每个人最终都会意识到,通过将事件循环阻塞操作卸载到另一个线程来减少 NodeJS 代码中的事件循环阻塞,结果却发现主线程的 CPU 负载翻了三倍。
为什么不使用structuredClone与worker通信?只要你的对象符合所有规则,就可以直接将其传递给postMessage。
structuredClone只会在本堆中创建一个新对象,而不是在隔离环境中创建新对象。每次发送消息时,你仍在底层进行字符串化/解析。
postMessage会透明地使用它:https://developer.mozilla.org/en-US/docs/Web/API/Worker/post…
因此,我好奇这些改进是否能应用于 structuredClone。
基准测试并未显示 structuredClone 在与 JSON 往返传输的对比中更具优势。这可能是因为与 JSON 兼容的数据结构比可克隆数据更简单。我怀疑此次更改后,JSON 将比 structuredClone 更快。
> 基准测试并未显示 structuredClone 在与 JSON 往返传输的对比中更具优势。
哪些测试?
根据此测试,structuredClone 与 FF 大致相当(在我的机器上 structuredClone 稍快,可能是误差范围):https://measurethat.net/Benchmarks/Show/23052/0/structuredcl… (Linux: FF 141, Chromium 138)。
现在只需用能编译为 WebAssembly 的语言编写处理代码,即可开始将 ArrayBuffers 复制并发送给 worker 线程!
或者我想也可以省略 WebAssembly 步骤。
为V8添加一个JSON.toBuffer方法将是个不错的补充。目前有几条代码路径似乎能实现这一功能,但据我所知,当前流程是Object→String→Buffer,为了提升性能,我们希望跳过中间步骤。
我实际上想象的是跳过对象步骤;如果你从网络 -> 缓冲区,并且只在缓冲区形式下处理它(即在 WebAssembly 中,使用更适合处理缓冲区/字节的语言),你就无需进行对象 -> JSON 步骤。尽管你在网络 -> 缓冲区步骤中可能需要做其他事情。
在JS中处理字节仍然很麻烦,我甚至不愿让我的死对头经历这种痛苦。就像我之前说的,如果你能自动化这个过程,情况会好得多。
当需要在 Node 中进行 严肃 的工作时,你就会开始使用 TypedArrays 和 SharedArrayBuffers,并直接处理二进制数据。字符串化主要用于玩具应用和小项目。
如果我把30年编程生涯中所有值得信赖的同事都召集起来,让他们处理这种位操作数组缓冲区的工作,我可能只能组建两家公司,其他人就完蛋了。
TypedArray 只是个玩具。很少有领域能真正与这种数据类型良好兼容。统计学领域可以。游戏?虽然游戏中经常使用,但游戏本身也充斥着被速通玩家利用的漏洞,因为游戏为了维持每秒处理更多数据的假象,往往会采取一些不严谨的处理方式。
DataView 稍好一些。我一直对人们在讨论 TypedArrays 和 SharedByteArrays 时,我竟然不知道 DataView 存在并且基本上一直存在感到惊讶。有人应该早很多、早很多就提到它。
DataView 有很多开销。直接用原始数据就好了。
最让我惊讶的是,浮点数序列化的性能在过去十年中有了多大的提升 [1].
[1] https://github.com/jk-jeon/dragonbox?tab=readme-ov-file#perf…
通过将IEEE浮点数转换为十进制UTF-8字符串再转换回浮点数的过程,不仅速度慢,而且极其脆弱。
二进制和十进制中精确可表示的值之间的差异意味着微小误差可能悄然渗入。
早在1990年,Steele和White就提出了一种实现完美往返转换的方法(他们可能并非第一个提出类似想法的人)。我猜他们的提案至少在2000年之前可能并不特别流行,与更传统的`printf`式舍入方法相比,但如今许多语言和平台似乎都将此类往返转换格式化算法作为默认选项提供。因此,我猜现在往返转换并不难,除非人们在不真正理解自己在做什么的情况下进行复杂操作。
“如何准确打印浮点数”,作者:Steele & White https://dl.acm.org/doi/10.1145/93548.93559
我认为楼主确实担心这类人。如今,高性能且正确舍入的 JSON 库已较为常见,但十年前并非如此(我认为)。
有趣!我之前并不知道 Steele 和 White 的 1990 年方法。不过我记得1996年Burger和Dybvig的方法。
你不需要精确地用十进制表示浮点数。你只需要确保每个浮点数都有唯一的十进制表示,只要包含足够的位数就能保证这一点:32位浮点数需要9位,64位浮点数需要17位。
https://randomascii.wordpress.com/2012/02/11/they-sure-look-…
您需要信任生成您消费的 JSON 或消费您生成的 JSON 的任何人,他们使用的库对这些表示形式的舍入规则达成一致。
请注意,消费者端其实没有太多模糊空间。你只需读取数字,计算其精确值,然后使用银行家舍入法将其舍入到最接近的二进制表示形式。除了在非常特殊的情况下,你不会做其他任何事情。几乎所有的模糊空间都存在于生产者端,可以通过使用任何具有往返保证的格式化算法来消除。
EDIT:如果你指的是十进制→二进制→十进制的往返转换,那完全是另一回事。
JSON本身并未强制要求使用IEEE754数值。
这是我经验中非常常见的误解之一。事实上,JSON 并未编码任何特定精度。它只是一个任意长度的十进制数,知道解析库可能会将其解码为类似 IEEE754 的格式。这就是为什么像 Python 的 json 这样的库允许你提供自定义解析器,例如,如果你希望将数字解析为 Decimal 对象。
无论你喜欢与否,JSON 数据类型本质上与 JavaScript 中可用的基本类型相关联。当然,你可以编写无法使用 JavaScript 原生类型处理的 JSON,但原生解析器始终会将数据反序列化为原生类型。直到最近,JavaScript 中所有数字都是 IEEE754 双精度浮点数,尽管现在确实存在任意精度的大整数。因此,JSON 中需要兼容的数字的实际精度限制是 IEE754。如果你控制客户端,你可以随心所欲地做任何事情。
标准确实限制了你可以期望处理的精度。
但不同解析器如何处理 JSON 数字可能会让你感到惊讶。这篇博客文章很好地详细说明了几个标准语言和库中做出的选择和细节:https://github.com/bterlson/blog/blob/main/content/blog/what…
我认为其中一个特别令人惊讶的点是,C# 和 Java 的标准解析器都使用 openAPI 模式提示,将数据类型标记为‘number’,从而将值映射为十进制浮点数类型,而非二进制类型。
>C# 和 Java 标准解析器
不确定您认为哪个解析器是标准的,因为Java本身并没有提供标准解析器(在标准库中)。除此之外,现有的解析器在反序列化时只接受目标类型(而非JSON),例如int、long等。
那篇博客文章将Jackson视为Java中事实上的标准JSON解析器/格式化器,这似乎是合理的。
这有点过头了——(不幸的是)代码库至少使用了4个不同的JSON库(如果算上一个非通用目的、亲自编写的库,可能有5个)。Gson也非常流行。博客文章提到了BigDecimal,到了那个时候,我不敢太信任它。
事实上的标准与大家使用Spring Boot的预期类似。
> 无论喜欢与否,JSON数据类型本质上与JavaScript中可用的基本类型相关联。
这显然取决于运行时环境。如果你不使用JavaScript,当然无需遵循JavaScript(或任何运行时环境)的解析器规范。
例如,我当前的实现明确使用任意精度小数来反序列化 JSON 数字。
确实——你可能在序列化到或从 JSON 中读取时,内存中的表示实际上是浮点数。JSON 并不关心这一点。
大多数常用语言(如 Python)早已解决了这个问题。对于任何非 NaN 的浮点数,将其转换为字符串再转换回原型,两者将完全相等。不仅如此,它们还能生成最短的字符串表示形式。
也许“荒谬地脆弱”这个词用得不对。或许“毫无必要地脆弱”更合适。
关键在于,这需要在JSON序列化/反序列化的两端都应用经过证明正确实现的算法。如果某一实现能正确处理自身的浮点数值,那当然很好——但JSON是一种互操作格式,那么当你将其发送到另一个系统并返回时,它还能正确处理吗?
这只是二进制浮点数序列化器无需担心的额外复杂性层。
确实,但许多系统在打印或解析此类数字时存在漏洞,一旦这些漏洞渗入,可能会引发长期问题。我记得曾因80年代末引入的解析器错误,不得不维护GIS软件中的替代坐标系和投影方式。
SWAR转义算法[1]与我几年前在Folly JSON中实现的算法[2]非常相似。后者基于8字节单词而非4字节,并且会返回需要转义的第一个字节的位置,从而确保在转义密集型字符串中快速路径不会增加明显开销。
[1] https://source.chromium.org/chromium/_/chromium/v8/v8/+/5cbc…
[2] https://github.com/facebook/folly/commit/2f0cabfb48b8a8df84f…
我认为V8没有得到足够的赞誉。如今JavaScript的速度简直令人难以置信
是的,这确实令人印象深刻!
这真是“只要有十亿美元,几乎什么问题都能解决”的真实例证 🙂
我更希望 JavaScript 继续演进(想想“严格模式”,但要“更严格”、“再严格一些”……),最终成为一种更简单、更易于编译/JIT 的语言。
我希望 JavaScript 拥有类型系统。有趣的是,由于运行时检查成本过高,无法为 JavaScript 添加类型系统,但实际上,导致 JavaScript 变慢的很大一部分原因正是需要不断检查类型,而唯一能提速的方法就是事后推断类型。我希望拥有类型系统,并添加一个“使用类型检查”功能,告知虚拟机我已经完成了某种程度的编译时检查,现在它只需进行那些无法在编译时完成的真正运行时检查。
棘手的是,虚拟机仍需在所有边界点进行检查,包括所有语言级和运行时级 API。在这样的迁移过程中,何时才能实现整体提速?
这需要整个社区和所有虚拟机实现者的大量支持,才能在合理时间内实现。我不是反对,只是指出这一点。
同意,但我认为所有主要库都会在任何类型安全的 JS 版本中很快被重写,尤其是如果它更快的话(假设这是事实)。
>运行时级别的 API
比如文档 API?
> 比如文档API?
是的,浏览器中的DOM、Node.js中的Node API等。
> 尤其是如果它更快的话
嗯,这就是问题所在。最初它会更慢,因为你调用的代码,以及调用你的代码,大多是不安全的。
除非你能不进行边界处的正确性检查就运行声音代码,否则此时代码的可理解性会降低,你可能会忍不住在代码中添加一些额外的运行时检查,而这些检查本应由类型系统捕获。
在这方面最可能的解决方案是一个 Typescript AOT 编译器,可能对你编写的代码有一些限制。
Porffor正在做这件事,JS -> WASM(作为中间表示)-> C -> 原生
对于TypeScript,它将类型作为编译器的提示,例如它有int类型,这些类型与number类型别名。
目前还处于非常早期的阶段,但非常酷。
https://porffor.dev/
编译成什么,WASM?
我没有考虑浏览器运行时。但也许有一天浏览器会支持原生 TypeScript?
> 但也许有一天浏览器会支持原生 TypeScript?
这可能已经存在;Node.js 通过忽略或去除代码中的类型来实现原生 TS 支持,而那些无法轻松去除的 TS 特性(如命名空间、枚举)如今已被废弃并不推荐使用。
TS 在运行时其实并不特殊。TS 类型用于类型检查,而类型检查不在运行时进行,运行 TS 代码时只是直接执行其中的 JavaScript 部分。
我们需要浏览器原生支持类型剥离。
对于非浏览器运行时,React Native 实际上正在开发一个 AOT TS/Flow 引擎。但他们发布的是字节码二进制文件,而非我提议的方案,尽管我提议的方案可能也难以实现。
> 我更希望 JavaScript 继续演进(想想“严格”模式,但更严格、再严格……)以成为一种更简单、更易于编译/JIT 的语言。
这就是/曾经是 ASM.js,它是 JavaScript 的一个有限子集,去掉了所有动态行为,这使得解释器可以跳过很多检查和假设。它已被 WASM 取代——基本上是在说,如果你需要严格性或性能,就使用另一种语言。
至于 JavaScript 的严格性,启用所有规则的 ESLint/Biome 也会使其变得严格。
ASM.js 和 ‘use strict’ 的目的完全不同。前者是性能优化工具,本应作为编译目标使用。后者则旨在通过禁用与可维护性原则冲突的功能,简化程序员的工作。
是的,这也是我想要的。给我一个“更严格”的模式。
就像 ASM.js 一样,在 WebAssembly 取代它之前?
不,ASM.js 和 ‘use strict’ 的目的完全不同且相互冲突。
另一方面,我认为V8是以一种奇怪的方式最极端的优化运行时,因为世界上只有大约100人理解它是如何工作的,而我们其他人则会想“为什么我的JS运行得这么慢”
还有一些人会说“为什么是 JavaScript”,然后开始一段陈旧的抱怨,最后在面试中离开,认为自己的拒绝是年龄歧视。
> 没有替换函数或空格参数:提供替换函数或空格/间隔参数用于美化打印是通用路径专属的功能。快速路径专为紧凑、未转换的序列化设计。
即使调用 `JSON.stringify(data, null, 0)` 也能得到这个结果吗?还是说参数必须是 undefined?
我认为不是。我使用 JSON.stringify(data, null, 0) 对一个 512KB 的 JSON 文件进行了测试,结果约为 2.4,而使用 JSON.stringify(data) 时约为 1.4。JSON.stringify(data, undefined, undefined) 与 JSON.stringify(data) 的结果相同。其他组合则更慢
https://microsoftedge.github.io/Demos/json-dummy-data/512KB…. Chrome 138.0.7204.184
令人惊叹的是,为了让这种糟糕的语言变得可忍受且高效,投入了如此多的努力。
如果我们从一个合理的起点开始,我们能走多远?
V8 非常出色,但(可能由于 JavaScript 本身?)它仍不及 LuaJIT 和 JVM 的性能。尽管至少对于 JVM 来说,它的预热时间比其他两个要长得多。
这是 JavaScript,据我所知,V8 比 LuaJIT 和 JVM 先进得多。
尽管 Java 也有优势,即不需要实时运行(即有编译器)
> 可能是因为 JavaScript 本身?
一针见血;JavaScript 的很多开销都源于其动态特性。asm.js 禁止了部分动态行为(如修改对象的形状,据我所记),因此可以跳过很多这类检查。
„甚至“
老兄,你可是顶尖高手。
那个“甚至”让我发笑——完全相同的“反应”
> 对象中不包含索引属性:快速路径针对具有常规字符串键的对象进行了优化。如果对象包含类似数组的索引属性(例如 ‘0’、'1' 等),则将由较慢的通用序列化器处理。
有什么想法吗?
我的猜测是,这些属性会影响属性排序,从而复杂化字符串化过程。
JavaScript 中对象属性的默认迭代规则规定,数值属性会首先按数值顺序遍历,随后再按添加到对象时的顺序遍历其他属性。由于数值需要按数值顺序而非字典顺序排列,引擎在排序前还需要将它们解析为整数。
我也感到好奇。他们是否在说,具有整数样式键的对象会被序列化为 JSON 数组?肯定不是吧?
我并不怀疑这项工作的价值,而且其性能直接影响常见操作的逻辑也合乎常理,但我更希望听到关于具体解决哪些问题的更多细节。V8生态系统中是否有关于
JSON.stringify
占用运行时资源的有趣数据?当每天有数亿个页面调用它时,它不需要主导运行时。全球范围内的节能效果将非常显著。
我真的很喜欢分段缓冲区的方法。这基本上就是我以前在用户空间中使用 fast-json-stringify 等库手动实现的绳数据结构技巧,现在原生实现且干净得多。你遇到过很多 bailout 条件吗?任何替换符、空格或自定义的 .toJSON() 方法会让你回到慢速路径吗?
无关紧要,但 v8.dev 网站快得惊人!我以为是通过链接悬停预加载内容,但不是。刷新
查看该网站,似乎是一个(静态?)HTML文件,以及共享的“main.css”和“main.js”文件。这两个文件都可以被浏览器缓存,因此每个页面只需下载几KB的压缩HTML。如果他们使用内容预加载,我认为我们在从一个页面导航到另一个页面时不会注意到太大的差异
这是我们在单页应用程序(SPA)出现前制作网站的方式,令人欣慰的是,即使在当今强大的CPU和高速网络环境下,这种方法仍能带来明显的性能提升。
该网站也非常简洁,没有广告或其他冗余内容,有点像Hacker News,同样速度很快。
其中或许蕴含着某种启示。
说到网站,有人知道这何时会应用到 Node 吗?Node 24 搭载了 V8 13.6,而这是 13.8… 我的意思是,这似乎是一个太大的性能提升,不应该只是在下次发布中加入,尤其是 Node 24 将是下一个 LTS 版本。
与其他生态系统相比如何?我已经使用 JSON 序列化大约十年了,它一直非常快,我从未真正考虑过这个问题。Simdjson 可以实现每秒每核处理数千兆字节。
一旦考虑预取、分支预测等因素,一个高度优化的 JSON 序列化器对于大多数实际工作负载来说应该几乎是免费的。
JSON 的缺点在于修改二进制数据时的 I/O 开销。如果每次用户修改一个布尔首选项时都必须将 100 兆字节的数据推送到块存储,那么无论你的序列化器有多快都无济于事。
我认为主要问题在于 JSON 能够序列化 JavaScript 对象,而这些对象通常比简单的哈希表复杂得多。
与主题无关,但我有一个最简单的网页设计改进建议:将<html>背景颜色改为#4285f4(与顶部栏相同),这样看起来就像一张蓝底的纸张。
是的……这样看起来确实更好。
哇,我不知道注册器内有打包的SIMD/SIMD(SWAR)。
我需要在回到电脑后,将此与我项目中通常使用的安全稳定的stringify包进行比较,但看到性能提升总是令人愉快的。
我是不是唯一一个觉得用“stringify”作为序列化数据结构的方法名有点……不太顺口的人?有点像米老鼠?我一直好奇为什么会选择这个词来表示如此“核心”的功能。
根据 JavaScript 的创建者布兰登·艾克[0]的说法:
> 2008 年 11 月 16 日 23:57,彼得·米肖写道:
> > “JSON.stringify” 这个名字对我来说有点太“Web 2.0”了。
> 才不是。这是黑客词典里的老派用语。
> > 为什么不使用更常见且听起来更严肃的选项,比如“serialize”?
> 呕,Web 1.0时代“我的第一个Java可序列化实现”的反怀旧气息。
[0]: https://web.archive.org/web/20100718152229/https://mail.mozi…
当最佳选项是“序列化”时,突然间“字符串化”似乎也没那么糟糕。我更倾向于一个听起来傻气但一目了然的函数,而非一个更抽象的概念。
这正是关键所在——这就是那个词:有点太像Web 2.0了……
序列化、Marshal、toString:永恒经典。Stringify:时尚潮流,转瞬即逝……
稍相关:每次看到
JSON.parse(JSON.stringify(object))
时,我都会感到一丝惋惜,因为与在更高效的语言中实现相比,这种做法效率低下。structuredClone(https://developer.mozilla.org/en-US/docs/Web/API/Window/stru… https://caniuse.com/?search=structuredClone) 提供基础支持(93% 的用户),但如果字段包含 DOM 对象或函数,它就无法正常工作,这意味着你可能需要在克隆前遍历并预处理对象,这样会更容易出错、更耗时且效率低下?
2022 年 3 月对于一个代码库来说并不算太久远。虽然需要时间,但 JavaScript 已经取得了长足的进步,并且无疑正在朝着正确的方向发展。
这还取决于代码库,如果你使用像 Vue 这样具有深度响应性的框架,你无法在不使用 toRaw 的情况下进行结构化克隆(因为它只在对象是浅层时有效),因为它会在代理对象上抛出异常。
Svelte 有 `$state.snapshot()`,我认为就是出于这个原因。
一如既往,双向字符串化算法的进步通常由 JSON 驱动(这次是 Dragonbox)。
我承认,我有点困惑,不知道在序列化时常见的副作用是什么?有没有我无意中忽略的明显原因类别?
一个简单的例子是
toJSON
。如果对象定义了该方法,它将被JSON.stringify自动调用,并可能产生任意副作用。我认为这更多是关于快速路径避免了可能产生副作用的操作(如toJSON),而非序列化过程中副作用本身常见。
文章对此有简要提及。
没错,你可以定义具有副作用的东西。我的问题是为什么这样做?将东西转换为 JSON 时,有哪些预期的可见副作用?
不过,我看到他们确实如你所说指出了这一点。我第一次读到时,以为他们会监控副作用。
我假设他们对所有标准类型都有一个允许列表。日期类型尤其如此,它们的toJson方法似乎仍然应该使用?(或者我在这点上也错了?:D)
调用属性获取器可能会有副作用,因此如果你序列化一个带有获取器的对象,必须非常小心,确保在序列化过程中不会发生奇怪的事情。
据我所知,人们曾利用此类副作用通过类型混淆攻击获取漏洞赏金。
没错,我明白这样做可能引发问题。但好奇为何要这么做? 😀
这通常是意外发生的。例如,假设你有一个名为 Person 的类,其中包含两个数据成员 firstName 和 lastName。我们已经遇到麻烦了,但让我们让情况更糟:它有一个名为 fullName 的 getter,该 getter 返回 $`{this.firstName} {this.lastName}`。
这个 getter 看起来无害,并且根据你的需求,它可能会正常工作。但它有副作用,因为字符串插值会分配内存并可能触发垃圾回收。
注意,如果你使用现代 JS 的 ‘class’ 块,‘get x ()’ 将被 JSON.stringify 忽略,因此如果你想重现这个场景,必须使用老式的 Object.defineProperty。
我并不认为这是副作用?也就是说,计算字段不一定是副作用,对吧?我更倾向于考虑那些记录自身被访问次数的对象。(这种模式最近在 Python 字典中被讨论过。)
我相信还有许多类似的用法是我尚未了解的。
不过,如果这确实包含计算字段,那范围似乎要广得多。
让我想起那句名言“快速行动”
我想看看使用 JSON.parse(JSON.stringify()) 与递归复制时,对象复制的速度对比如何。
在这次比较中也考虑 `structuredClone()`;我不明白为什么解析/序列化仍然存在,这只是一个权宜之计。我确信可以为这个特定用例创建一个特殊的代码路径,使其超级快速,或者推断出它需要创建对象的完整副本,但不应该有这样的路径。
一个未被讨论的重要问题是,通用路径是否会因需要先检查快速路径是否可用而变得更慢。
并非如此——通用路径是快速路径的备用方案,它从快速路径停止的地方继续执行,因此无需对整个对象进行快速性检查(且在到达备用方案前仍可使用快速路径)
我认为JSON序列化已经相当快,因此这已经很好了。上次我将JSON序列化与Protocol Buffers进行比较时,JSON在典型场景下只是稍微慢一点,但差异并不显著。此类优化可以改变性能的平衡。
JSON 是一种优秀的极简格式,既可供人类阅读,又可供机器解析。我一直不明白 Protocol Buffers 的流行原因;二进制格式对可读性是一个重大妥协。我理解有些人欣赏类型验证功能,但它为传输协议层增加了大量复杂性和摩擦。
对我来说,Protocol Buffers 的吸引力在于其线格式的前向兼容性和后向兼容性。
保持逻辑兼容性已经够难的了,所以我欣赏不需要过多考虑传输兼容性这一点。当然,你可以用 JSON 解决同样的问题,但,嗯,你必须自己解决。
(值得一提的是,我对 gRPC 生态系统中有很多不喜欢的地方,所以我实际上并不经常使用它。但这是我非常喜欢的一个部分。)
可以说 JSON 根本不存在这个问题,因为它也会编码字段名称。它唯一无法处理的是字段重命名,但你知道,在公共 API 中你本来就不能重命名字段,对吧?
我感谢这条评论。
确实有些技术似乎因解决自己制造的问题而获得赞誉。
> 机器可读
它确实可读,但并非高效格式。IEEE754与字符串之间的转换即使使用所有优化和改进仍成本高昂。字节数组也缺乏良好的呈现方式。
我认为JSON的压缩会比ProtoBuf增加显著的开销,再加上额外的内存使用。
我也不否认人们对ProtoBuf有点过于热衷。
一种格式不可能同时兼顾人类和机器可读性。JSON是人类可读的,这就是它的意义所在。人类可读性仅在调试时有用,但它存在开销,因为它不适合机器处理。Protobuf 消息既更小又更快速解码。如果你处于每秒处理数百万条消息的环境中,二进制格式会带来显著优势。人类查看的消息数量微乎其微,因此没有必要保留那个较慢的处理路径。只需编写一个消息转储工具即可。
嗯,随着大语言模型(LLMs)的出现,“机器可读性”的定义已经发生了很大变化。
速度总是好的!
不错
> 优化底层临时缓冲区
那么用数组列表代替数组吗?
你是否对解析器常见的安全问题进行了任何测试/回归?似乎该解决方案可能存在未来产生 CVE 的风险
…你认为 V8 没有对可能成为全球执行次数最多的用户空间代码路径之一进行测试吗?