该材料是我们今天发布的翻译的第一部分,将讨论V8 JavaScript引擎如何选择代表内存中各种JS值的最佳方法,以及这如何影响V8的内部机制(所谓的
表单)对象 (形状)。 所有这些都将帮助我们理清最近的
React性能问题的实质。

JavaScript数据类型
每个JavaScript值只能具有八个现有数据类型之一:
Number ,
String ,
Symbol ,
BigInt ,
Boolean ,
Undefined ,
Null和
Object 。
JavaScript数据类型值的类型可以使用
typeof运算符确定,但是有一个重要的例外:
typeof 42;
如您所见,尽管
null具有自己的类型
typeof null ,但
typeof null命令返回
'object'而不是
'null' 。 为了理解这种类型的行为的原因,我们考虑到所有JavaScript类型的集合可以分为两组的事实:
- 对象(即,键入
Object )。 - 原始值(即任何非客观值)。
根据这种知识,结果表明
null表示“无对象值”,而
undefined表示“无值”。
原始值,对象,空值和未定义遵循Java精神的这些思考,Brendan Eich设计了JavaScript,以便
typeof运算符将为右图中的那些类型的值返回
'object' 。 所有对象值和
null都在这里。 这就是为什么表达式
typeof null === 'object'为true的原因,尽管语言规范中存在单独的类型
Null 。
v ==='object'的表达式typeof为true价值表示
JavaScript引擎应该能够表示内存中的所有JavaScript值。 但是,请务必注意,JavaScript中的值类型与JS引擎在内存中表示它们的方式是分开的。
例如,JavaScript中的值42为
number类型。
typeof 42;
有几种方法可以在内存中表示像42这样的整数:
根据ECMAScript标准,数字是64位浮点值,称为双精度浮点数(Float64)。 但是,这并不意味着JavaScript引擎总是将数字存储在Float64视图中。 那将是非常非常低效的! 引擎可以使用数字的其他内部表示形式-只要值的行为与Float64数字的行为完全匹配即可。
事实证明,实际JS应用程序中的大多数数字都是有效的ECMAScript数组
索引 。 即-在0到2
32 -2范围内的整数。
array[0];
JavaScript引擎可以选择最佳格式来表示内存中的此类值。 这样做是为了优化使用索引使用数组元素的代码。 执行内存访问操作的处理器需要将数组索引作为可存储在视图中的数字
加2来使用 。 相反,如果我们以Float64值的形式表示数组的索引,则将浪费系统资源,因为每当有人访问数组元素时,引擎都需要将Float64数字转换为加2的格式,反之亦然。
32位数字加上最多两个数字的表示不仅对优化数组工作很有用。 通常,应该指出,处理器执行整数运算的速度比使用浮点值的运算快得多。 这就是为什么在以下示例中,没有问题的第一个周期是第二个周期的两倍。
for (let i = 0; i < 1000; ++i) {
这同样适用于使用数学运算符的计算。
例如,操作员从下一个代码片段中获取余数除法的性能取决于计算中涉及的数字。
const remainder = value % divisor;
如果两个操作数均由整数表示,则处理器可以非常有效地计算结果。 对于
divisor操作数由2的幂表示的数,V8中还有其他优化。 对于用浮点数表示的值,计算要复杂得多,并且要花费更长的时间。
由于整数运算通常比对浮点值进行运算要快得多,因此似乎引擎可以始终将所有整数和整数运算的所有结果简单地以加两个的格式存储。 不幸的是,这种方法会违反ECMAScript规范。 如前所述,该标准以Float64格式提供数字表示,并且某些使用整数的运算可能会导致以浮点数形式出现结果。 在这种情况下,JS引擎产生正确的结果很重要。
即使在前面的示例中,表达式左侧的所有数字都是整数,表达式右侧的所有数字都是浮点值。 这就是为什么使用32位格式(最多增加两个)无法正确执行以前的操作的原因。 JavaScript引擎必须特别注意以确保在执行整数运算时,您可以获得正确的(虽然看起来像上例一样,但看起来与众不同)Float64结果。
如果小整数落在有符号整数的31位表示形式的范围内,则V8使用称为
Smi的特殊表示形式。 不是
Smi值的所有内容都表示为
HeapObject值,这是内存中某个实体的地址。 对于不属于
Smi范围的数字,我们有一种特殊的
HeapObject所谓的
HeapNumber 。
-Infinity
从上一个示例中可以看到,一些JS数字表示为
Smi ,而有些则表示为
HeapNumber 。 V8引擎在处理
Smi编号方面进行了优化。 事实是,小整数在实际的JS程序中非常常见。 使用
Smi值时,不必为单个实体分配内存。 此外,使用它们还可以使您快速执行整数运算。
Smi,HeapNumber和MutableHeapNumber的比较
让我们谈谈这些机制的内部结构是什么样的。 假设我们有以下对象:
const o = { x: 42,
对象
x的属性的值42被编码为
Smi 。 这意味着它可以存储在对象本身内部。 另一方面,要存储值4.2,则需要创建一个单独的实体。 在对象中,将有一个指向该实体的链接。
储存各种价值假设我们正在执行以下JavaScript代码:
ox += 10;
在这种情况下,属性
x的值可以在其存储位置进行更新。 事实是
x的新值是52,并且这个数字落在
Smi的范围内。
属性x的新值存储在先前值存储的位置。但是,新的
y值5.2不适合
Smi的范围,此外,它与以前的y-4.2值不同。 结果,V8必须为新的
HeapNumber实体分配内存,并已从对象中引用它。
新实体HeapNumber存储新的y值HeapNumber实体是不可变的。 这使您可以进行一些优化。 假设我们要将对象
x的属性设置为属性
y x值:
ox = oy;
执行此操作时,我们可以简单地引用相同的
HeapNumber实体,而不分配额外的内存来存储相同的值。
HeapNuber实体的抗扰性的缺点之一是,频繁更新具有
Smi范围之外的值的字段的速度很慢。 在以下示例中对此进行了演示:
处理第一行时,将创建
HeapNumber的实例,其初始值为0.1。 在循环的主体中,此值更改为1.1、2.1、3.1、4.1,最后更改为5.1。 结果,在执行此代码的过程中,
HeapNumber 6个
HeapNumber实例,其中五个实例在循环完成后将进行垃圾回收操作。
堆号实体为了避免出现此问题,V8进行了优化,这是一种用于在数值已存储的相同位置更新数值不符合
Smi范围的数值字段的机制。 如果数字字段存储的
Smi实体不适合存储的值,则V8以对象的形式将该字段标记为
Double并为
MutableHeapNumber实体分配内存,该实体存储以Float64格式表示的实数值。
使用MutableHeapNumber实体结果,在字段值更改之后,V8不再需要为新的
HeapNumber实体分配内存。 相反,只需将新值写入现有的
MutableHeapNumber实体。
将新值写入MutableHeapNumber但是,这种方法有其缺点。 即,由于
MutableHeapNumber的值可以更改,因此重要的是要确保系统以这些值按照语言规范提供的方式工作。
MutableHeapNumber的缺点例如,如果将
ox的值分配给其他变量
y ,则
y的值必须不随
ox的后续更改而更改。 那将违反JavaScript规范! 结果,在访问
ox ,必须将数字重新打包为通常的
HeapNumber值,然后才能将其分配为
y 。
对于浮点数,V8使用其内部机制执行上述打包操作。 但是对于小整数,使用
MutableHeapNumber将浪费时间,因为
Smi是表示此类数字的更有效方法。
const object = { x: 1 };
为了避免系统资源的低效率使用,我们要做的小整数就是将对象形式的对应字段标记为
Smi 。 因此,只要这些字段的值与
Smi范围相对应,就可以直接在对象内部对其进行更新。
使用其值在Smi范围内的整数待续...
亲爱的读者们! 您是否遇到过由JS引擎功能引起的JavaScript性能问题?
