无法在对象浏览器中查看此项目_面试官:我们来聊聊Chrome中的V8隐藏类吧

前言

本文是关于V8内部工作原理的翻译类文章,主要讲解了V8内部的数据表示方式-隐藏类,并给出具体代码编写中的优化策略。当然,欢迎大家关注我,我将持续分享哪些前端层面核心的知识点,希望能给同处前端的你带来一点点收获。

1、文章概述

几周之前,我们开始了一系列旨在深入挖掘JavaScript及其实际工作原理的研究:我们认为通过了解JavaScript的构建块以及它们如何共同发挥作用,您将能够编写更好的代码和应用程序。

前面写了两篇《高手进阶之史上最全JS内存管理策略剖析》和《浏览器事件循环必知必会10点》关于浏览器内部原理的文章。 这篇文章将深入探讨谷歌V8 JavaScript引擎的内部原理,并提供一些关于如何编写更好的JavaScript代码的最佳实践。

JavaScript引擎是执行JavaScript代码的程序或解释器。 JavaScript引擎可以实现为标准解释器,或即时编译器,其中后者将JavaScript编译为字节码。下面是实现了JavaScript引擎的热门项目列表:

V8 - 由Google开发的开源软件,用C ++编写

Rhino - 由Mozilla Foundation管理,开源,完全用Java开发

SpiderMonkey - 第一个支持Netscape Navigator的JavaScript引擎,现在支持Firefox

JavaScriptCore - 开源,以Nitro销售,由Apple为Safari开发

KJS - KDE的引擎最初由Harri Porten为KDE项目的Konqueror Web浏览器开发

Chakra(JScript9) - Internet Explorer

Chakra(JavaScript) - Microsoft Edge

Nashorn,作为OpenJDK的一部分的开源,由Oracle Java Languages and Tool Group编写

JerryScript - 是物联网的轻量级引擎。

2、为什么需要V8引擎

由谷歌构建的V8引擎是开源的,用C ++编写。 此引擎在Google Chrome中使用。 然而,与其他引擎不同,V8也用于流行的Node.js运行时环境。

6e0b33e1e74e860e0962f28b220bfe90.png

V8最初旨在提高Web浏览器中JavaScript执行的性能。 为了获得速度,V8将JavaScript代码转换为更高效的机器代码,而不是使用解释器。 它通过实现像许多现代JavaScript引擎(如SpiderMonkey或Rhino(Mozilla))的JIT(即时)编译器,在执行时将JavaScript代码编译为机器代码。 这里的主要区别是V8不产生字节码或任何中间代码

3、V8使用两个解析器

在V8版本5.9出现之前,该引擎使用了两个编译器:

full-codegen - 一个简单而快速的编译器,可以生成简单且相对较慢的机器代码。

Crankshaft - 一种更复杂的(即时)优化编译器,可生成高度优化的代码。

V8引擎内部还使用了几个线程:

1)主线程:获取代码,编译代码然后执行它。

2)独立的编译线程:单独的线程用于编译,因此当这个线程在优化代码的时候主线程依然可以继续执行。

3)Profiler线程:它能检测到运行时花费大量时间的方法,以便Crankshaft可以优化它们

4)额外线程:用来处理垃圾回收相关事务

首次执行JavaScript代码时,V8利用full-codegen直接将解析后的JavaScript转换为机器代码。这使它可以非常快速地开始执行机器代码。请注意,V8不使用中间字节码表示,因此无需解释器。

当代码运行一段时间后,Profiler线程已经收集了足够的数据来告诉应该优化哪个方法。接下来,Crankshaft重新开启一个线程用于代码优化。它将JavaScript抽象语法树转换为名为Hydrogen的更高层的静态单赋值的表示形式,又名SSA(high-level static single-assignment representation),并尝试优化Hydrogen图。

4、代码优化之内联

第一个优化是提前内联尽可能多的代码, 内联是直接调用函数体内的代码而不是调用函数本身。 这个简单的步骤允许以下优化更有意义。

9f9706383edfc73d81161384fda7b88e.png

5、Hidden类

5.1 、Hidden类要解决的问题

fbce7571ef6a135388f0a1616f00696d.png

上图表示JAVA语言访问属性的例子:

也就是说它可以共享Class info ,很好理解因为Java 不是动态脚本,运行时不能为类对象添加属性。它的 Class info可以在指定内存地址保存固化。访问成员对象属性先访问到info 表,获取得到属性对应的地址偏移后,通过指针偏移得到属性值。而对于JS对象来说,它的对象压根就是通过哈希表存储,存上key和value完事,key被hash,value是值地址。比如下面的代码:

function Point(x, y) { this.x = x; this.y = y;}var p1 = new Point(10, 11);var p2 = new Point(12, 13);

在内存中存储后得到如下的内容:

814a6111292d862a4ad66f72d9420b60.png

Point的成员属性都是相同的,但是被分散存储了。每个对象取值都要hash key,然后再找到其value。而hash本身的速度较慢,因此才有了通过Hidden Class来提速的情况,这样就不需要在访问每一个JS对象的对应的key的时候都做一次hash key。

在 JavaScript中对象是以Hash结构存储的,用键值对表示对象的属性,Key 的数据类型为字符串,Value 的数据类型是`结构体`,即对象是以 类型的HashMap结构存储的。

5.2、隐藏类详解

JavaScript也是一种动态编程语言,这意味着可以在实例化后轻松地在对象中添加或删除属性。

大多数JavaScript解释器使用类似字典的结构(基于散列函数)来存储对象属性值在内存中的位置。这种结构使得在JavaScript中检索属性的值比在Java或C#等非动态编程语言中的计算成本更高。在Java中,所有对象属性都是在编译之前确定的,并且无法在运行时动态添加或删除,因此,属性值(或指向这些属性的指针)可以作为连续缓冲区存储在存储器中,每个缓冲区之间具有固定偏移量。因此偏移的总长度也可以通过属性类型轻松确定,而这在JS中几乎是不可能的,因为JS的属性值和类型可以在运行时被动态修改

由于使用字典在内存中查找对象属性的位置效率非常低,因此V8使用不同的方法:隐藏类。隐藏类的工作方式类似于Java等语言中使用的固定对象布局(fixed object layouts),但是隐藏类本身是在运行时创建的。现在,让我们看看它们实际上是什么样的:

function Point(x, y) { this.x = x; this.y = y;}var p1 = new Point(1, 2);

当new Point(1, 2)被调用的时候,V8创建了一个Hidden类为"C0":

5042dac89d9d6f5ce69782d6c5c22d95.png

因为此时没有为C0设置任何属性,因此它是空的。

一旦执行了第一个语句“this.x = x”(在“Point”函数内),V8将创建一个名为“C1”的第二个隐藏类,它基于“C0”。 “C1”描述了可以找到属性x的存储器中的位置(相对于对象指针)。 在这种情况下,“x”存储在偏移0处,这意味着当在存储器中查看Point对象作为连续缓冲区时,第一偏移将对应于属性“x”。 V8还将使用“类转换”更新“C0”,该类转换指出如果将属性“x”添加到Point对象,则隐藏类应该从“C0”切换到“C1”。 下面的点对象的隐藏类现在是“C1”。

8c7ad26368a979c2e0fc706f79001cb7.png

每次将新属性添加到对象时,旧的隐藏类都会添加一个属性来描述新隐藏类的转换路径。 隐藏类转换很重要,因为它们允许在以相同方式创建的对象之间共享隐藏类。 如果两个对象共享一个隐藏类并且同一属性被添加到它们中,则转换将确保两个对象都接收相同的新隐藏类以及随后作用到其上的优化代码。

执行语句“this.y = y”时重复此过程(在Point函数内,在“this.x = x”语句之后)。创建一个名为“C2”的新隐藏类,将类转换添加到“C1”,声明如果将属性“y”添加到Point对象(已包含属性“x”),则隐藏类应更改为 “C2”,Point对象的隐藏类更新为“C2”。

8088db9e23509b898f17ba93dd815afb.png

隐藏类转换取决于属性添加到对象的顺序。 看一下下面的代码片段:

function Point(x, y) { this.x = x; this.y = y;}var p1 = new Point(1, 2);p1.a = 5;p1.b = 6;var p2 = new Point(3, 4);p2.b = 7;p2.a = 8;

对于“p1”,首先添加属性“a”,然后添加属性“b”。 但是,对于“p2”,首先分配“b”,然后是“a”。 因此,作为不同转换路径的结果,“p1”和“p2”以不同的隐藏类结束。 在这种情况下,以相同的顺序初始化动态属性要好得多,以便可以重用隐藏类

6、内联缓存

V8利用另一种技术来优化动态类型语言,称为内联缓存。内联缓存依赖于一个事实:对相同方法的重复调用往往发生在相同类型的对象上。可以在此处找到有关内联缓存的深入说明。

那么它是怎样工作的? V8维护一个在最近的方法调用中作为参数传递的对象类型的缓存,并使用此信息来假设将来作为参数传递的对象类型。如果V8能够对将传递给方法的对象类型做出很好的假设,它可以绕过确定如何访问对象属性的过程,而是使用先前查找到对象的存储信息,即隐藏类。

那么隐藏类和内联缓存的概念是如何关联的呢?每当在特定对象上调用方法时,V8引擎必须执行对该对象的隐藏类的查找,以确定访问特定属性的偏移量。在将同一方法成功调用两次到同一个隐藏类之后,V8省略了隐藏类查找,只是将属性的偏移量添加到对象指针本身。对于该方法的所有未来调用,V8引擎假定隐藏类未更改,并使用先前查找中存储的偏移直接跳转到特定属性的内存地址。这大大提高了执行速度。

内联缓存也是为什么相同类型的对象共享隐藏类非常重要的原因。如果你创建两个相同类型和不同隐藏类的对象(正如我们之前的例子中所做的那样),V8将无法使用内联缓存,因为即使这两个对象属于同一类型,它们对应的隐藏类也是为其属性分配不同的偏移量。

6d0adfecb39ec38d7677013b72b51652.png

这两个对象基本相同,但“a”和“b”属性是按不同顺序创建的。

7、编译成机器码

当Hydrogen图优化后,Crankshaft将其转化为更加底层的表示方式,即Lithium。大多数Lithium实现都是特定于体系结构的。寄存器分配发生在此层级。

最后,Lithium被编译成机器代码。然后发生了一些叫做OSR(on-stack replacement)的事情:堆叠替换。在我们开始编译和优化一个耗时运行的方法之前,我们可能正在运行它。 V8将转换我们拥有的所有上下文(堆栈,寄存器),以便我们可以在执行过程中切换到优化版本。这是一项非常复杂的任务,请记住,除了其他优化之外,V8也做了内联优化。 V8并不是唯一能够做到这一点的引擎。

8、垃圾回收

对于垃圾收集,V8采用传统的标记和扫描方式来清理。 标记阶段应该停止JavaScript执行。 为了控制GC成本并使执行更稳定,V8使用增量标记:其非遍历整个堆,尝试标记堆中每个可能的对象,而是遍历部分堆,然后恢复正常执行。 下一个GC停止将从上一个堆遍历停止的位置继续。 这允许代码在正常执行期间非常短暂的暂停以完成GC。 然而,须记住如前所述,扫描阶段是由单独的线程处理的

9、Ignition和TurboFan

随着2017年早些时候发布V8 5.9,引入了新的执行管道。 这个新的管道在实际的JavaScript应用程序中实现了更大的性能提升和显著的内存节省。新的执行管道建立在Ignition(V8的解释器)和TurboFan(V8的最新优化编译器)之上。

自从V8的5.9版本问世以来,V8已经不再使用full-codegen和Crankshaft(自2010年以来为V8提供服务的技术),因为V8团队一直在努力跟上新的JavaScript语言功能并作出相应的优化策略。这也意味着整体V8将具有更简单,更易维护的架构。

1682933b80bd3d3d4099c771e9e2f2b9.png

这些改进只是一个开始。 新的Ignition和TurboFan管道为进一步优化铺平了道路,这些优化将在未来几年内提升JavaScript性能并缩小V8在Chrome和Node.js中的占用空间。

最后,这里有一些关于如何编写优化良好的JavaScript的技巧。

1)对象属性的顺序:始终以相同的顺序实例化对象属性,以便可以共享隐藏的类和随后优化的代码。

2)动态属性:在实例化之后向对象添加属性将强制隐藏类更改,并丢失为先前隐藏类任何方法作出的优化。建议在构造函数中分配所有对象的属性。

3)方法:重复执行相同方法的代码将比仅执行一次许多不同方法的代码运行得更快(由于内联缓存)。

4)数组:避免key不是自增数的稀疏数组。没有填满元素的稀疏数组是哈希表。元素访问更加昂贵。另外,尽量避免预先分配大的数组,而是按需申请。最后,不要尝试删除数组中的元素使其稀疏。

5)标记值:V8表示32比特来表示对象和数字。它使用一个比特来表示它是一个对象(flag = 1)还是一个称为SMI(SMall Integer)的整数(flag = 0)。然后,如果数值大于31位,V8会将其变为双精度并创建一个新对象以将数字放入其中。所以,尝试尽可能使用31位带符号的数字,以避免对JS对象进行昂贵的装箱操作。

参考资料:

  • https://github.com/thlorenz/v8-perf
  • http://code.google.com/p/v8/wiki/UsingGit
  • http://mrale.ph/v8/resources.html
  • https://www.youtube.com/watch?v=UJPdhx5zTaw
  • https://www.youtube.com/watch?v=hWhMKalEicY
  • https://blog.sessionstack.com/how-javascript-works-inside-the-v8-engine-5-tips-on-how-to-write-optimized-code-ac089e62b12e
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值