当前位置: 首页 > 后端技术 > Node.js

V8JavaScript中的元素种类和性能优化

时间:2023-04-03 20:30:24 Node.js

原文:V8JavaScript对象中的“元素种类”可以具有与之关联的任意属性。对象属性的名称可以包含任何字符。JavaScript引擎可以优化的一个有趣示例是属性名称是纯数字时,一种特殊情况是数组索引属性。在V8中,如果属性名称是数字(最常见的形式是Array构造函数生成的对象),则会对其进行特殊处理。尽管在许多情况下,这些数字索引属性的行为与其他属性类似,但出于优化目的,V8选择将它们与非数字属性分开存储。在引擎内部,V8甚至给这些属性起了一个特殊的名字:elements。对象具有映射到值的属性,而数组具有映射到元素的索引。尽管这些内部结构从未直接暴露给JavaScript开发人员,但它们解释了为什么某些代码模式比其他代码模式更快。常见元素类型在运行JavaScript代码时,V8会跟踪每个数组包含哪些元素。此信息有助于V8优化对数组元素的操作。例如,当您在数组上调用reduce、map或forEach时,V8可以根据数组包含的元素优化这些操作。以这个数组为例:constarray=[1,2,3];它包含什么样的元素?如果您使用typeof运算符,它会告诉您该数组包含数字。在语言级别,这就是您得到的:JavaScript不区分整数、浮点数和双精度数——它们只是数字。但是,在引擎层面,我们可以做出更精确的区分。该数组的元素是PACKED_SMI_ELEMENTS。在V8中,术语Smi指的是一种用于存储小整数的特定格式。(我们将在稍后的PACKED部分进行解释。)稍后向该数组添加一个浮点数以将其转换为更通用的元素类型:constarray=[1,2,3];//元素类型:PACKED_SMI_ELEMENTSarray.push(4.56);//元素类型:PACKED_DOUBLE_ELEMENTS将字符串添加到数组会再次更改其元素类型。constarray=[1,2,3];//元素类型:PACKED_SMI_ELEMENTSarray.push(4.56);//元素类型:PACKED_DOUBLE_ELEMENTSarray.push('x');//元素类型:PACKED_ELEMENTS三个不同的元素,具有以下基本类型:小整数,也称为Smi。双精度浮点数、浮点数、不能用Smi表示的整数。不能表示为Smi或双精度值的常规元素。请注意,double是Smi的更一般变体,而常规元素是对double的另一种概括。可以表示为Smi的数字集是可以表示为double的数字的子集。这里的重点是元素类型转换仅在一个方向上起作用:从特定的(例如PACKED_SMI_ELEMENTS)到更一般的(例如PACKED_ELEMENTS)。例如,一旦一个数组被标记为PACKED_ELEMENTS,它就不能退回到PACKED_DOUBLE_ELEMENTS。到目前为止,我们已经了解了以下内容:V8为每个数组分配了一个元素种类。数组的元素种类没有绑定在一起——它可以在运行时改变。在前面的示例中,我们从PACKED_SMI_ELEMENTS转换为PACKED_ELEMENTS。元素类型转换只能从特定类型转换为更通用的类型。PACKED与HOLEY密集阵列PACKED和稀疏阵列HOLEY。到目前为止,我们只处理了密集或压缩(PACKED)数组。在数组中创建稀疏数组会将元素降级为其HOLEY变体:constarray=[1,2,3,4.56,'x'];//元素类型:PACKED_ELEMENTSarray.length;//5数组[9]=1;//array[5]untilarray[8]arenowholes//Elementtype:HOLEY_ELEMENTSV8造成这种差异的原因是对PACKED数组的操作比对HOLEY数组的操作更利于优化。对于PACKED数组,大多数操作都可以高效执行。相比之下,对HOLEY数组的操作需要额外的检查和昂贵的原型链查找。到目前为止我们看到的每个基本元素(即Smis、double和regular)都有两种类型:PACKED和HOLEY。我们不仅可以从PACKED_SMI_ELEMENTS转换为PACKED_DOUBLE_ELEMENTS,还可以从任何PACKED形式转换为HOLEY形式。回顾一下:最常见的元素类型是PACKED和HOLEY。对PACKED数组的操作比对HOLEY数组的操作更有效。元素类型可以从PACKED过渡到HOLEY。elementskindlatticeV8将这个转换系统实现为一个lattice(数学概念)。这是一个简化的可视化,仅显示最常见的元素类型:仅向下过渡通过晶格。一旦将一个单精度浮点数添加到Smi数组中,即使该浮点数稍后被Smi覆盖,它也会被标记为DOUBLE。类似地,一旦在数组中创建了一个洞,它就会被永久标记为HOLEY,即使它稍后被填充也是如此。V8目前有21种不同的元素类型,每种都有自己的一组可能的优化。通常,更具体的元素类允许进行更细粒度的优化。元素类型在网格中越靠下,对象运行越慢。为了获得最佳性能,请避免不必要的非特定类型-坚持适合您情况的最特定类型。性能提示在大多数情况下,元素种类跟踪隐藏在幕后,您无需担心。但是,您可以执行以下几项操作以充分利用您的系统。同样:更具体的元素种类允许更细粒度的优化。元素类型在网格中越靠下,对象运行越慢。为了获得最佳性能,请避免不必要的非特定类型-坚持适合您情况的最特定类型。避免创建空洞假设我们正在尝试创建一个数组,例如:constarray=newArray(3);//此时,数组是稀疏的,所以它被标记为`HOLEY_SMI_ELEMENTS`//即给出当前信息最具体的可能性。数组[0]='a';//接下来,这是一个字符串,而不是一个小整数...所以过渡到`HOLEY_ELEMENTS`。array[1]='b';array[2]='c';//此时,数组中的三个位置都被填充,因此数组被打包(即不再稀疏)。//但是,我们不能转换为更具体的类型,例如“PACKED_ELEMENTS”。//元素类保留为“HOLEY_ELEMENTS”。一旦一个数组被标记为有空洞,它就永远是空洞——即使它被打包了!从那时起,对数组的任何操作都可能变慢。如果您计划对数组执行大量操作并且希望优化这些操作,请避免在数组中创建空洞。V8可以更高效地处理密集数组。更好的创建数组的方法是使用字面量:constarray=['a','b','c'];//elementskind:PACKED_ELEMENTS如果不知道前面元素的所有值时间,你可以创建一个空数组,然后压入值。constarray=[];//...array.push(someValue);//...array.push(someOtherValue);此方法可确保数组不会被转换为多孔元素。因此,V8可以更有效地优化数组上的任何操作。避免读取超出数组的长度当读取超出数组的长度时,例如读取array[42]时,会发生类似array.length===5的情况。在这种情况下,数组索引42超出范围,数组本身不存在该属性,因此JavaScript引擎必须执行同样昂贵的原型链查找。不要这样写你的循环://不要这样做!for(leti=0,item;(item=items[i])!=null;i++){doSomething(item);}这段代码读取数组中的所有元素,然后再次读取。停止,直到找到未定义或null的元素。(jQuery在几个地方使用了这种模式。)相反,以老式的方式编写循环,只是迭代到最后一个元素。for(letindex=0;index{doSomething(item);});现在,for-of和forEach的性能都可以与老式的for循环相媲美。避免读取超出数组的长度!这样做就像数组中的一个洞一样糟糕。在这种情况下,V8的边界检查失败,属性是否存在的检查失败,然后我们需要查找原型链。避免元素类型转换通常,如果您需要对数组执行大量操作,请尽量坚持使用特定的元素类型,以便V8可以尽可能地优化这些操作。这比看起来更难。例如,只需将-0添加到数组中,小整数数组会将其转换为PACKED_DOUBLE_ELEMENTS。常量数组=[3,2,1,+0];//PACKED_SMI_ELEMENTSarray.push(-0);//PACKED_DOUBLE_ELEMENTS因此,此数组上的任何操作都将以与Smi完全不同的方式进行优化。避免使用-0,除非您需要在代码中清楚地区分-0和+0。(您可能不需要)还有NaN和Infinity。它们表示为双精度,因此添加NaN或Infinity会将SMI_ELEMENTS转换为DOUBLE_ELEMENTS。constarray=[3,2,1];//PACKED_SMI_ELEMENTSarray.push(NaN,Infinity);//PACKED_DOUBLE_ELEMENTS如果打算对整型数组进行大量操作,请考虑初始化时归一化-0,防止NaN和无穷。这样数组将保存PACKED_SMI_ELEMENTS。事实上,如果您对数组进行数学运算,请考虑使用TypedArray。每个数组都有专门的元素类型。类数组对象与数组JavaScript中的一些对象——尤其是DOM中的对象——看起来像数组,尽管它们并不是真正的数组。你可以自己创建一个类数组对象:constarrayLike={};arrayLike[0]='a';arrayLike[1]='b';arrayLike[2]='c';arrayLike.length=3;该对象具有长度并支持索引元素访问(就像数组一样!),但它缺少原型上的forEach之类的数组方法。尽管如此,仍然可以调用数组泛型:Array.prototype.forEach.call(arrayLike,(value,index)=>{console.log(`${index}:${value}`);});//This记录“0:a”,然后是“1:b”,最后是“2:c”。这段代码的工作方式如下,在类数组对象上调用Array的内置Array.prototype.forEach。但是,这比在真实数组上调用forEach慢,引擎数组的forEach在V8中进行了高度优化。如果你打算在这个对象上多次使用数组内置函数,考虑先把它变成一个真正的数组:constactualArray=Array.prototype.slice.call(arrayLike,0);actualArray.forEach((value,index)=>{console.log(`${index}:${value}`);});//这会记录'0:a',然后是'1:b',然后最后'2:c'。对于进行一次性转换的成本值得后续优化,特别是如果您计划对数组执行大量操作。例如,arguments对象是一个类数组对象。可以在其上调用数组内置函数,但此类操作不会得到完全优化,因为这些优化仅针对真实数组。constlogArgs=function(){Array.prototype.forEach.call(arguments,(value,index)=>{console.log(`${index}:${value}`);});};logArgs('a','b','c');//这会记录'0:a',然后是'1:b',最后是'2:c'。ES2015的rest参数在这里很有用。它们产生真正的数组,可以优雅地替代类数组对象参数。constlogArgs=(...args)=>{args.forEach((value,index)=>{console.log(`${index}:${value}`);});};logArgs('a','b','c');//这会记录'0:a',然后是'1:b',最后是'2:c'。如今,没有理由直接使用对象参数。通常,应尽可能避免使用类似数组的对象,而应使用真正的数组。避免多态如果你的代码需要处理包含许多不同元素类型的数组,它可能比单个元素类型数组慢,因为你的代码在不同类型的数组元素上是多态的。考虑以下示例,其中使用了各种元素种类调用。(请注意,这不是原生的Array.prototype.forEach,它有一些自己的优化,不同于本文讨论的元素类优化。)consteach=(array,callback)=>{for(letindex=0;indexconsole.log(item);each([],()=>{});each(['a','b','c'],doSomething);//`each`使用`PACKED_ELEMENTS`调用。V8使用内联缓存//(或“IC”)来记住`each`是用这种特定的//元素类型调用的。V8是乐观的,并假设//`each`函数中的//`array.length`和`array[index]`访问是单态的(即只接收一种//元素),直到证明并非如此。对于以后每次调用`each`,V8都会检查元素种类是否为`PACKED_ELEMENTS`。如果//是,V8可以重新使用之前生成的代码。如果不是,则需要更多工作//。each([1.1,2.2,3.3],doSomething);//`each`是用`PACKED_DOUBLE_ELEMENTS`调用的。因为V8现在已经//看到不同的元素类型传递给其IC中的`each`,//`a`each`//函数内部的rray.length`和`array[index]`访问被标记为多态。V8现在每次调用`each`时都需要额外的////检查:一个用于`PACKED_ELEMENTS`//(像以前一样),一个新的检查用于`PACKED_DOUBLE_ELEMENTS`,一个用于//任何其他元素类型(像以前一样)。这会导致性能//hit.each([1,2,3],doSomething);//`each`是用`PACKED_SMI_ELEMENTS`调用的。这会触发另一个//多态性程度。对于“each”,IC中现在有三种不同的元素//种类。对于从现在开始的每个`each`调用,//还需要进行另一个元素类型检查,以将生成的代码//重新用于`PACKED_SMI_ELEMENTS`。这是以性能成本为代价的。内置方法(例如Array.prototype.forEach)可以更有效地处理这种多态性,因此在对性能敏感时考虑使用它们而不是用户库函数V8中单态性与多态性的另一个例子涉及对象形状,也称为隐藏类的对象。要了解更多信息,请查看Vyacheslav的文章。调试元素种类要找出给定对象的“元素种类”,请使用d8的调试版本(请参阅“从源代码构建”)并运行:$out.gn/x64.debug/d8--allow-natives-syntax这会打开d8REPL中的特殊功能,例如%DebugPrint(object)。输出中的“元素”字段显示您传递给它的任何对象的“元素种类”。d8>常量数组=[1,2,3];%debugprint(array);debugprint:0x1fbbad30fd71:[jsarray]-地图=0x10a6f8a038b1[fastproperties]-prototype=0x1212bb687ec1-elements=0x1fbbad30330fd71cow=3-lients=3-portersies=3-properties=3-properties=3--(constaccessordescriptor)}-elements=0x1fbbad30fd19{0:11:22:3}[…]请注意,“COW”表示写时复制,这是另一个内部优化.现在不用担心-这是另一篇博文的主题!调试版本中另一个有用的标志是--trace-elements-transitions。启用它可以让V8在任何元素发生类型转换时通知您。$catmy-script.jsconstarray=[1,2,3];array[3]=4.56;$out.gn/x64.debug/d8--trace-elements-transitionsmy-script.jselements转换[PACKED_SMI_ELEMENTS->PACKED_DOUBLE_ELEMENTS]in~+34atx.js:2for0x1df87228c911从0x1df87228c889到0x1df87228c941