V8 架构优势总结
1、什么是V8引擎?
V8使用C++开发,并在谷歌浏览器中使用。
在运行JavaScript之前,相比其它的JavaScript的引擎转换成字节码或解释执行,V8将其编译成原生机器码(IA-32, x86-64, ARM, or MIPS CPUs)
并且使用如内联缓存(inline caching)方法来提高性能。
有了这些改进,JavaScript程序在V8引擎下的运行速度媲美二进制程序。
v8版本发布历程
2017-4-27 发布v5.9
2018-8-7 发布v6.9
2019-8-13 发布v7.7
2019-9-27 发布v7.8
2019-11-20 发布v7.9
2019-12-18 发布v8.0
在 V8 的 5.9 版本出来之前使用两个编译器:
full-codegen - 一个简单而且速度非常快的编译器,可以生成简单且相对较慢的机器代码。
Crankshaft(曲柄轴) - 一种更复杂(Just-In-Time)的优化编译器,生成高度优化的代码。
V8 内部使用多个线程:
主线程,完成您期望的任务:获取代码,编译并执行它
编译线程,一个线程用于编译,以便主线程可以继续执行,而前者正在优化代码
分析线程,一个Profiler 线程用于分析,它会告诉运行时花费很多时间,让 Crankshaft 优化它们
垃圾收集器,其他线程用于处理,垃圾收集器
2、其他JS解析引擎有哪些?
V8,Google开发,C++编写,应用在Node和Chrome中
SpiderMonkey(蜘蛛猴) 第一个JavaScript引擎,网景开发,应用在Firefox中
JavaScriptCore 苹果公司的Safari中
Chakra (JScript9) Edge浏览器
Chakra (JavaScript) IE9-IE11
3、V8在新版本中的优化有哪些?
loop-invariant
(将循环内不变代码移到外面,减少每次循环执行代码量)V8
的额外负担)function Point(x,y){ this.x = x; this.y = y; } var p1 = new Point(1,2)
//此处在执行 new Point()时,v8开始创建一个隐藏类 C0
分析隐藏类执行过程:
第一句 “this.x = x” 被执行时V8 将创建第二个隐藏类,名为“C1”,基于“C0”。 “C1”描述了可以找到属性x在内存中的位置(相对于对象指针)。
在这种情况下,“x”被存储在0处,因此在内存中将对象看作一段连续存储空间时,第一个地址将对应属性“x”。
V8 也会用“class transition”来更新“C0”,如果一个属性“x”被添加到一个对象时,隐藏类应该从“C0”切换到“C1”。
下面的点对象的隐藏类现在是“C1”。
每当一个新的属性被添加到一个对象时,旧的隐藏类会被更新到新的隐藏类的转换路径,称为隐藏类的转换。
隐藏类的转换非常重要,因为它允许隐藏类以相同方式创建的对象之间共享。
增加属性y隐藏类转换为C2.
隐藏类转换取决于给对象赋值属性的顺序。
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\p2,p1和p2的赋值顺序不同,以不同的隐藏类别结束,这种情况隐藏类无法被复用,如果赋值顺序相同则可以复用隐藏类。
3.3、内联缓存
内联缓存优化 动态类型语言。
内联缓存依赖于观察,重复调用的观察,相同类型对象上相同方法的重复调用。
V8 维护一个在最近的方法调用中作为参数传递的对象类型的缓存,并使用这些信息来预测将来作为参数传递的对象的类型。
如果V8能够很好地假定传递给方法的对象类型,那么它可以绕过如何访问对象的属性的过程,而是将之前查找到的信息用于对象的隐藏类。
无论何时在特定对象上调用方法时,V8 引擎都必须执行对该对象隐藏类的查找。
以确定访问特定属性的偏移量。
在同一个隐藏类的两次成功的调用之后,V8 省略了隐藏类的查找,并简单地将该属性的偏移量添加到对象指针本身。
对于该方法的所有下一次调用,V8 引擎都假定隐藏的类没有更改
并使用从以前的查找存储的偏移量直接跳转到特定属性的内存地址,提高了执行速度。
内联缓存也是为什么相同类型的对象可以共享隐藏类非常重要的原因。
如果你创建了两个相同类型的对象和不同的隐藏类(就像我们之前的例子中那样),V8 将不能使用内联缓存
使用内联缓存条件:即使两个对象是相同的类型,它们相应的隐藏类为其属性分配不同的偏移量;要求不仅要类型相同、还要顺序相同。
当 Hydrogen 图被优化,Crankshaft 将其降低到 Lithium 低级表示。
大部分 Lithium 实现都是特定于架构,寄存器分配发生在这个级别。
最后Lithium 被编译成机器码。
然后是 OSR :on-stack replacement(堆栈替换)。
在我们开始编译和优化一个明确的长期运行的方法之前,我们可能会运行堆栈替换。
V8 不只是缓慢执行堆栈替换,并再次开始优化。相反,它会转换我们拥有的所有上下文(堆栈,寄存器)
以便执行过程中切换到优化版本。
这是一个非常复杂的任务。
有一种相反的变换,被称为去优化的保护措施做出相反变换,并假设引擎优化无效,还原成非优化代码。
对于垃圾收集,V8 采用了传统的分代式扫描方式来清理老一代。
标记阶段应该停止 JavaScript 的执行。为了控制 GC(garbage collection) 成本并使执行更加稳定,V8 使用了渐进式标记:而不是走遍整个堆内容,试图标记每一个可能的对象。它只走一部分堆内容,然后恢复正常执行。下一个 GC 将从先前堆走过的地方继续执行。这允许在正常执行期间非常短的暂停。如前所述,扫描阶段由不同的线程处理。
对象属性的顺序:始终以相同的顺序实例化对象属性,以便共享的隐藏类和随后优化的代码可以共享之。
动态属性:在实例化之后向对象添加属性将强制执行隐藏的类更改,并降低之前隐藏类所优化的所有方法的执行速度。在构造函数中分配所有对象属性更好。
方法:重复执行相同方法的代码将比仅执行一次的多个不同方法(由于内联缓存)的代码运行得更快,所以应该尽量复用函数。
数组:避免稀疏数组,其中键值不是自增的数字。并没有存储所有元素的稀疏数组是哈希表。这种数组中的元素访问开销较高。另外,尽量避免预分配大数组。最好是按需增长。最后,不要删除数组中的元素。这会使键值变得稀疏。
标记值:V8 使用 32 位表示对象和数值。由于数值是 31 位的,它使用了一位来区分它是一个对象(flag = 1)还是一个称为 SMI(SMall Integer)整数(flag = 0)。那么,如果一个数值大于 31 位,V8会将该数字装箱,把它变成一个双精度数,并创建一个新的对象来存放该数字。尽可能使用 31 位有符号数字,以避免对 JS 对象的高开销的装箱操作。