当前位置: 首页 > 科技观察

深入理解JavaScript引擎本质

时间:2023-03-20 15:10:46 科技观察

本文来自GoogleEngineV8工程师Mathias和Benedikt在JSConfEU2018的演讲,涵盖了所有JavaScript引擎共有的一些关键基础知识。作为一名JavaScript开发人员,深入了解JavaScript引擎的工作原理将有助于您了解所编写代码的性能特征。全文由五个部分组成:1.JavaScript引擎工作流程:介绍JavaScript引擎的处理流水线。这部分会涉及解释器/编译器的内容,会介绍不同引擎之间的区别和共性;2..JavaScript对象模型;3.属性访问优化:介绍引擎如何通过Shapes、Transisionchainsandtrees、ICs等概念优化对象属性的获取;4.高效存储数组;5.Takeaways:全文内容做了总结,并给出了两点建议。JavaScript引擎工作流程JavaScript引擎解析源代码并将其转换为抽象语法树(AST),基于AST,解释器可以开始工作并生成字节码,而引擎正在执行JavaScript代码。为了使其执行得更快,可以将字节码与分析数据一起发送到优化编译器。优化编译器根据现有的分析数据做出某些假设,然后生成高度优化的机器代码。如果在某个时候某个假设被证明是不正确的,优化编译器将取消优化并回退到解释器部分。JavaScript引擎中的解释器/编译器流程现在,让我们关注实际执行JavaScript代码的流程部分,即代码被解释和优化的部分,并讨论主要JavaScript引擎之间的一些差异。一般来说,所有的JavaScript引擎都有一个由解释器和优化编译器组成的进程。其中,解释器可以快速生成未优化的字节码,而优化编译器则需要更长时间才能最终生成高度优化的机器码。这个大体流程与Chrome和Node.js中使用的V8引擎工作流程几乎相同:V8中的解释器称为Ignition,它负责生成和执行字节码。它在运行字节码时收集分析数据,然后可用于加速(代码)执行。当一个函数变得很热时,即它被频繁调用时,生成的字节码和分析数据将传递给TurboFan-我们的优化编译器,它会根据分析数据生成高度优化的机器代码。在Firefox和SpiderNode中使用的MozillaJavaScript引擎SpiderMonkey有一些不同之处。他们有两个优化编译器。解释器将代码解释给基线编译器,基线编译器可以生成部分优化的代码。结合代码运行时收集的分析数据,IonMonkey编译器可以生成高度优化的代码。如果优化尝试失败,IonMonkey将回退到基线阶段代码。在Edge和Node-ChakraCore项目中使用的微软JavaScript引擎Chakra也对两个优化编译器有类似的设置。解释器将代码优化为SimpleJIT——其中JIT代表即时编译器——它可以生成部分优化的代码。结合分析数据,FullJIT可以生成更深度优化的代码。JavaScriptCore(缩写为JSC)是Apple在Safari和ReactNative中使用的JavaScript引擎,使用三种不同的优化编译器进行了优化。低级解释器LLInt解释代码并传递给Baseline编译器,(经过Baseline编译器)将优化后的代码传递给DFG编译器,(经过DFG编译器处理后)最终将结果传递给FTL编译器进行进一步处理。处理。为什么有些引擎有更多的优化编译器?这都是一个权衡。解释器可以快速生成字节码,但字节码的效率往往不够高。另一方面,优化编译器处理需要更长的时间,但最终会产生更高效的机器代码。在快速获取可执行代码(解释器)或花费更多时间但最终以最佳性能运行代码(优化编译器)之间存在平衡。一些引擎选择添加多个具有不同耗时/效率特征的优化编译器,以更高的复杂性为代价,以对这些权衡点进行更细粒度的控制。我们刚刚强调了每个JavaScript引擎中解释器和优化编译器流程的主要区别。除了这些差异之外,所有JavaScript引擎都具有相同的架构:它们都有一个解析器和某种解释器/编译器管道。JavaScript对象模型通过关注某些方面的具体实现,让我们看看JavaScript引擎还有哪些共同点。例如,JavaScript引擎是如何实现JavaScript对象模型的,它们使用了哪些技巧来加速获取JavaScript对象的属性?事实证明,所有主要引擎在这一点上都有类似的实现。ECMAScript规范基本上将所有对象定义为将字符串键映射到属性属性的字典。除了[[Value]],规范还定义了以下属性:[[Writable]]决定属性是否可以被重新赋值;[[Enumerable]]判断属性是否出现在for-in循环中;[[Configurable]]确定是否可以删除该属性。[[squarebrackets]]的表示法可能看起来有点奇怪,但这是规范定义不直接暴露给JavaScript的属性的方式。在JavaScript中,仍然可以通过Object.getOwnPropertyDescriptorAPI获取指定对象的属性值:constobject={foo:42};Object.getOwnPropertyDescriptor(object,'foo');//→{value:42,writable:true,enumerable:true,configurable:true}JavaScript定义了对象,那么数组呢?您可以将数组视为一组特殊的对象。两者之间的一个区别是数组对数组索引有特殊处理。这里所说的数组索引是ECMAScript规范中的一个特殊术语。在JavaScript中,数组被限制为最多包含2^32-1个项目。数组索引是该限制内的任何有效索引,它是从0到2^32-2的任何整数。另一个区别是数组还有一个神奇的长度属性。constarray=['a','b'];array.length;//→2array[2]='c';array.length;//→3本例生成数组,长度单位为2.然后我们将另一个元素分配给索引2,并且长度属性会自动更新。JavaScript以类似于对象的方式定义数组。例如,包括数组索引在内的所有键值都被显式表示为字符串。数组中的第一个元素存储在键值“0”下。“长度”属性恰好是另一个不可枚举和不可配置的属性。将元素添加到数组后,JavaScript会自动更新“长度”属性的[[Value]]属性值。通常,数组的行为也与对象非常相似。优化属性访问让我们深入了解JavaScript引擎如何有效地处理与对象相关的操作。查看JavaScript程序,访问属性是最常见的操作之一。JavaScript引擎能够快速获取属性至关重要。constobject={foo:'bar',baz:'qux',};//在这里,我们访问属性`foo`on`object`:doSomething(object.foo);//^^^^^^^^^^Shapes在JavaScript程序中,多个对象具有相同的key-value属性是很常见的。这些物体都具有相同的形状。constobject1={x:1,y:2};constobject2={x:3,y:4};//`object1`和`object2`具有相同的形状。访问具有相同形状的对象的相同属性也很常见:functionlogX(object){console.log(object.x);//^^^^^^^^}constobject1={x:1,y:2};constobject2={x:3,y:4};logX(object1);logX(object2);考虑到这一点,JavaScript引擎可以根据对象的形状优化对象属性的获取。这就是它的工作原理。假设我们有一个具有属性x和y的对象,它使用我们之前讨论的字典数据结构:它包含以字符串表示的键值,并且它们指向各自的属性值。如果访问一个属性,比如object.y,JavaScript引擎会在JSObject中查找key值'y',然后加载对应的属性值,最后返回[[Value]]。但是这些属性值是如何存储在内存中的呢?我们应该将它们存储为JSObject的一部分吗?假设我们稍后会遇到更多相同形状的对象,在JSObject本身中存储属性名称和属性值的完整字典是浪费(空间),因为我们正在为所有相同形状的对象重复该属性姓名。它过于冗余并引入了不必要的内存使用。作为优化,引擎将对象的Shape单独存储。Shape包含除[[Value]]之外的所有属性名称和其余属性。相反,Shape包含JSObject内部值的偏移量,以便JavaScript引擎知道到哪里寻找特定值。每个具有相同形状的JSObject都指向这个Shape实例。现在每个JSObject只需要存储那些对那个对象来说是唯一的值。当我们有多个对象时,优势变得清晰可见。不管有多少个对象,只要它们的形状相同,我们只需要存储一次它们的形状和键值属性信息!所有JavaScript引擎都使用形状作为优化,但称呼不同:学术论文称它们为HiddenClasses(容易与JavaScript中类的概念混淆)V8称它们为Maps(容易与JavaScript中Map的概念混淆)Chakra调用它们类型(容易与动态类型和JavaScript中的关键字typeof混淆)JavaScriptCore称它们为StructuresSpiderMonkey称它们为Shapes在本文中,我们将继续称它们为形状。过渡链和树如果您有一个具有特定形状的对象,但您向它添加了一个属性,会发生什么情况?JavaScript引擎是如何找到这个新形状的?constobject={};object.x=5;object.y=6;在JavaScript引擎中,形状的表示称为过渡链。下面是一个例子:对象初始化时没有任何属性,所以它指向一个空的形状。下一条语句向对象添加一个值为5的属性“x”,因此JavaScript引擎转向一个包含属性“x”的Shape,并将值5添加到JSObject的第一个偏移量为0处。next语句添加属性“y”,引擎转向另一个包含“x”和“y”的Shape,并将值6附加到JSObject(在偏移量1处)。我们甚至不需要为每个Shape存储完整的属性表。相反,每个Shape只需要知道它引入的新属性。例如在这个例子中,我们不必在最后一个Shape中存储关于'x'的信息,因为它可以在链上更早地找到。为此,每个Shape将连接到它的前一个Shape:如果您在JavaScript代码中编写o.x,JavaScript引擎将沿着转换链寻找属性“x”,直到找到引入属性“x”的Shape.但是,如果我们不能只创建一个转换链呢?例如,如果您有两个空对象并为每个对象添加了不同的属性会怎么样?constobject1={};object1.x=5;constobject2={};object2.y=6;在这种情况下,我们必须进行分支,最终得到一个过渡树而不是过渡链:在这里,我们创建一个空对象a,然后向其添加属性“x”。我们最终得到一个包含单个值的JSObject和两个形状:一个空形状和一个仅包含属性x的形状。第二个示例也以一个空对象b开始,但后来添加了一个不同的属性“y”。我们最终得到两个形状链,总共三个形状。这是否意味着我们总是需要从一个空的constobject1={};开始?,正如我们之前所见。在object2的情况下,直接生成具有属性x的对象是有意义的,而不是从一个空对象开始然后进行转换连接。包含属性“x”的对象文字以包含“x”的形状开始,有效地跳过空形状。V8和SpiderMonkey(至少)正是这样做的。这种优化缩短了转换链并使从文字构造对象更加高效。Benedikt的博客文章React应用程序中令人惊讶的多态性讨论了这些微妙之处如何影响现实世界的性能。内联缓存(IC)形状背后的主要动机是内联缓存或IC的概念。IC是让JavaScript变快的关键!JavaScript引擎使用IC来记住在哪里寻找对象属性以减少昂贵的查找次数。这是一个函数getX,它接受一个对象并从中提取属性x的值:来自第一个参数(arg1)的属性“x”值并将其存储在地址loc0中。第二条指令返回我们存储到loc0中的内容。JSC还在get_by_id指令中嵌入了InlineCache,它由两个未初始化的槽组成。现在假设我们使用对象{x:'a'}调用getX函数。正如我们所知,这个对象有一个Shape和一个属性“x”,它存储属性x的偏移量和其他属性。当您第一次执行该函数时,get_by_id指令将查找属性“x”并找到其存储在偏移量0处的值。get_by_id指令中嵌入的IC存储该属性的形状和偏移量:对于后续运行,IC只需比较形状,如果与之前相同,则只需从记住的偏移量加载属性值。具体来说,如果JavaScript引擎发现一个对象的形状之前已经被IC记录下来,它就不再需要触及属性信息——而是可以完全跳过昂贵的属性信息查找(过程)。这比每次查找属性要快得多。高效存储数组数组存储数组索引等属性是很常见的。这些属性的值称为数组元素。为每个数组中的每个数组元素存储属性属性会浪费存储空间。相反,由于数组索引默认属性是可写、可枚举和可配置的,JavaScript引擎通过将数组元素与其他命名属性分开存储来利用这一点。考虑这个数组:constarray=['#jsconfeu',];引擎存储数组长度(1)并指向具有偏移量和“长度”属性的形状。这与我们之前看到的类似……但是数组值存储在哪里?每个数组都有一个单独的元素后备存储,其中包含所有数组索引的属性值。JavaScript引擎不必为数组元素存储任何属性属性,因为它们通常是可写、可枚举和可配置的。那么,如果不是通常情况呢?如果更改了数组元素的属性怎么办?//请不要这样做!constarray=Object.defineProperty([],'0',{value:'Ohnoes!!1',writable:false,enumerable:false,configurable:false,});上面的代码片段定义了一个名为'0'的属性(恰好是一个数组索引),但是它的属性(值)被设置为非默认值。在这种边缘情况下,JavaScript引擎会将整个元素后备存储表示为字典映射数组索引到属性属性。即使只有一个数组元素具有非默认属性,整个数组的后备存储处理也会进入这种缓慢且低效的模式。避免在数组索引上使用Object.defineProperty!(我不知道你为什么要这样做。这似乎是一件奇怪且毫无价值的事情。)要点我们已经了解了JavaScript引擎如何存储对象和数组,以及Shapes和IC如何针对它们的常见操作进行优化.基于这些知识,我们确定了一些有助于提高性能的有用JavaScript编码技巧:始终以相同的方式初始化对象,以确保它们不会出现不同的形状方向。不要混淆数组元素的property属性,以确保它们可以有效地存储和操作。相关链接英文原文:https://mathiasbynens.be/notes/shapes-ics知乎翻译:https://zhuanlan.zhihu.com/p/38202123