首页 > 解决方案 > 浏览器是否总是将 Javascript 中的字符串和数字视为不可变的?

问题描述

在 JavaScript 中,浏览器运行时解释器是否总是将字符串和数字视为不可变的?

当然,如果它被证明是无害的,他们会优化并将它们视为可变的。如果没有,为什么不呢?

例如,考虑不起眼的 for 循环。

for (let i = 0; i < 1000000000000; i++) {
 console.log(i)
}

由于变量的作用域是循环,并且循环中的任何代码都不需要 i 变量的“旧值”,因此浏览器只需增加符号指向每次迭代i的数字是有意义的。i否则,一个新字节的内存流将被 的新值占用,这将是i不可想象的原因(“有人可能需要i! 的旧值”)。我们将在 for 循环(i在内存中创建新值)和垃圾收集器(杀死所有旧值i)之间进行不必要的竞争,循环通常会获胜,并且我们将发生堆栈溢出。

哦,就是这样,不是吗。如果是这样,为什么浏览器在以其他方式优化代码方面如此聪明,却以这种方式愚蠢?

字符串也有类似的情况。考虑以下。

{
   let completeWorks = "This string dictates the complete works of William Shakespeare. To be or not to be that is the question whether it is nobler in the mind..."
   completeWorks += "The End."  // <-- what happens here?
}

该字符串completeWorks是块范围的,并且可以证明只存在于该块中。所以当浏览器遇到指令时,completeWorks += "The End"它肯定会发生变化completeWorks。如果不是,为什么不呢?他们不这样做可能有一个很好的理由,我想学习它。

标签: javascriptperformancememory-managementgarbage-collectionv8

解决方案


(这里是 V8 开发人员——因此我对其他浏览器/引擎知之甚少。)

对此没有简单的答案。实现很复杂。

在 V8 中,字符串始终是不可变的(在创建之后)。一个原因是,在堆上分配对象时,对象后面通常没有可用空间,因此我们不能只将字符附加到现有字符串。另一个原因是,跟踪哪些字符串可以安全地被突变会增加非常多的复杂性(除了一些更容易检测的小众案例,但如果只支持这些,那么该机制提供的价值就会少得多)。

V8 确实有一些巧妙的字符串操作技巧:当您获取较大字符串的子字符串时,不会复制任何字符;新字符串只是一个引用,上面写着“我是那里另一个字符串的长度为 X 的切片,从索引 Y 开始”。同样,当像您的completeWorks示例一样连接两个字符串时,新字符串是一个引用,上面写着“我是其他两个字符串的连接”。(为了完整起见,我会提到有最小字符数低于这些技巧不会应用,因为简单地复制字符至少同样有效。)

与字符串相比,数字对性能更敏感,也更容易处理。一般来说,堆分配的数字总是不可变的。但这不是故事的结局。V8 大量使用“Smis”(“小整数”)的特殊表示,因为 JavaScript 程序中的许多数字都属于该类别。Smis 不是堆对象;创建一个新的和修改一个一样便宜,而且实际上无法区分(就像一个int在 C++ 中)。对于超出 Smi 范围的数字,优化编译器还执行“转义分析”并可以“拆箱”非转义数字,这意味着将它们保存在 CPU 寄存器中(作为普通的 64 位浮点数)而不是在堆上分配它们首先,这再次比改变其他不可变的堆对象更好。对于存储在对象属性中的数字的特殊情况,V8 也(在某些情况下)使用可变存储。

因此,您的问题的答案是“是”(例如,在生成未优化的代码时,V8 不会花时间执行分析,因此代码必须保守地假设某处需要任何旧值)和“否”(对于优化编译器,您的直觉是正确的,这应该是可以避免的;但这并不意味着在堆上分配的任何数字都会在那里发生变异)。

由于i变量的范围是循环

JavaScript 中的作用域很复杂。首先,没有int i. 现在考虑一下:

for (var i = 0; i < 100; i++) {
  // Use i here, or don't.
}
console.log(i);  // Prints "100".

如果您的意思是let i,那么可以肯定的是,您将拥有一个块范围的变量。在此示例中,性能将是相同的。

我们将在 for 循环(i在内存中创建新值)和垃圾收集器(杀死所有旧值i)之间进行不必要的竞争,循环通常会获胜

不会。垃圾收集器是高度自适应的,尤其是在发生更多分配时它会做更多的工作。没有办法“超越”它。如果需要,程序执行会在垃圾收集器尝试查找可以释放的内存时停止。

我们将有一个堆栈溢出。

不,堆栈溢出通常与对象分配、垃圾收集或堆内存无关。


推荐阅读