前端能力模型-V8 JS引擎

来源:互联网 发布:工业乙醇的蒸馏数据 编辑:程序博客网 时间:2024/05/16 09:57
一、webkit内核与V8

在chrome浏览器中,用webkit来进行html渲染,用v8作为js引擎。
     虽说Chrome和Webkit都是开源的,但是Chrome始终保持和Webkit距离,Chrome在WebKit上封装了一层称为WebKit Glue。Glue层中,大部分类型的结构和接口都和WebKit类似,Chrome中依托WebKit组件,都只是调用Webkit Glue层的接口,而不是直接调用WebKit中的类型。按照chrome自己文档来说,虽然我们再用webkit实现页面渲染,但通过WebKit Glue中间层,某种程度上大大降低与webkit的耦合,blink就是一个很好的可替代webkit的渲染引擎。
     V8充分发挥了研发HotSpot和Strongtalk所获得的知识。

二、android webkit

android的webkit包括java层和c层,两者由Java Native Interface实现通信,示例图如下:



三、WebView
     
     webView是处于Java层的视图模块,通常在Android Native App中插入的html页面也是构建于WebView之上,包括了页面的浏览器,请求的处理。这也就是为什么WebView的出镜率比Android Webkit本身还要高,很多Native App在开发的时候,部分更新率高的模块,都会选择使用WebView来渲染html页面,从而可以方便内容更新。

     在C层中也有一个WebView模块,C层中的WebView模块负责初始化并构造WebView对象,然后将其赋值给Java层的WebView。之后两者就可以进行通信。


四、Safari与chrome内核差异对比



js引擎早期慢有一定的历史原因,当时只用在网页少数动画,交互操作,浏览器的开发优先提升渲染引擎的速度,js处理速度不是很重要。
随着富应用井喷式出现,对js的依赖和要求越来越高,性能问题便再次称为网络应用开发者最关心的。


五、javascript与c++、java的不同

1)语言本身的问题

C++和Java采用静态类型(static typing)。代码编译时,宣告变量类型,JS却需要在执行期间检查数据类型,因此静态类型占有性能上的优势。

2)object变量和方法在js、c++、java的差异性

以图例来说明:




C++、Java处理object的变量和方法,以它们的名称1:1对应数组内的位移值储存在数组中。会事先知道要存取的变量类型,因此只用数组和位移就可以存取变量和方法。

js中,个别对象都有自己属性和方法等表格。每次程序存取属性或是呼叫方法时,都必须检查对象的类型并执行适当处理。

许多JavaScript引擎都使用哈希表(hash table)来存取属性和寻找方法等。换言之,每次存取属性或是寻找方法时,就会使用字符串作为寻找对象哈希表的键(key),如下图:




图中属性存取时的内部javascript处理:使用对象x哈希表的字符串foo作为搜寻foo内容的关键字。
搜寻哈希表是一个连续动作,包含从散列(hashing)值中判定数组内位置,然后查看该位置的键值(key)是否相等。然后可以使用位移直接读取数据的数组比较起来,利用此方法存取较费时。

使用动态类型的其他语言,还有Smalltalk和Ruby等。这些语言基本上也是搜寻哈希表,但它们利用类来缩短搜寻时间。然而,Js没有类,除了Numbers指示数字值、Strings为字符串以及其他几个基本类型外,剩下对象都是object型。无法声明类,因此无法使用明确的类型来加速处理。

六、V8引擎加速技术

     js的弹性允许在任何时间,在对象上新增或是删除属性和方法。业界一般认为动态语言比静态语言更难加速。V8利用了好几项技术来达到加速目的:

1)JIT编译(JIT Compile):不用字节码(bytecode)生成机器语言

     从性能角度看,V8具有4个主要特性,首先,它在执行时以称为及时(just-in-time, JIT)的编译方法,来产生机器语言。这是个普遍由来改善解释速度的方法,在java中也可以发现此方法。V8比Firefox中的SpiderMonkey JavaScript引擎,或Safari的JavaScriptCore等竞争引擎还要早的实践了这一技术。

     v8 JIT编译器在产生机器语言时,不会产生中间码。例如,在Java编译器先将原始码转换成一个以虚拟中间语言(称为字节码,bytecode)表示的一类文件(class file)。Java编译器和字节码编译器产生字节码,而非机器语言。Java VM按顺序地在执行解释字节码。此执行模式称为字节码解释器(bytecode interpreter)。Firefox的SpiderMonkey具有一个内部的字节码编译器和字节解释器,将JS原始码转换成它自家特色的字节代码,以便执行。如下图:



     程序语言系统先使用语法分析器将原始码转换成抽象语法树(abstract syntax tree, AST)。之前有几种方式来处理。字节码编译器将抽象语法树编译为中间代码,然后在编译器中执行。如Java JIT等混合模式将这中间代码的一部分编译成机器语言,以改善处理性能。Chrome不使用中间代码,JIT直接从抽象语法树来编译机器语言。也有抽象语法树解释器,直接解析抽象语法树。

     事实上,Java VM目前使用一个以HotSpot为基础的JIT编译器。它扮演字节码解释器的角色,来解析代码,将常执行的代码区块转换成机器语言然后执行,这就是混合模式(hybrid model)。

     字节码解释器、混合模式等,具有制作简单且有绝佳可移植性的优点。只要是引擎可以编译的原始码,那么就可以在任何CPU架构上执行字节码,这正是为什么该技术被称为【虚拟机(VM)】的原因。即使在产生机器代码的混合模式中,可以借由编写字节码的解释器开始进行开发,然后实现机器语言生成器。通过使用简单的位元码,在机器代码产生事,要将输出最佳化就变得容易许多。

     V8不是将原始程序转换成中间语言,而是将抽象语法直接产生机器语言并加以执行。没有虚拟机,且因为不需要中间表示式,程序处理会更早开始。不过,它也丧失了虚拟机的好处,例如透过字节码解释器和混合模式等,所带来的高可移植性(portability)和优化的简易性等。

2)垃圾回收管理:Java标准特性的精妙实现

     第二个关键的特性是,V8将垃圾回收管理(garbage collection, GC*)实作为【精确的GC*】,相反的,大部分的JS引擎、Ruby及其他语言编译器都是使用保守的GC*(conservative GC),因为保守的GC实作简单许多。虽然精确的GC更为复杂,但也有性能上的优点。Oracle(Sun)的Java VM就是使用精确GC。

     Garbage collection (GC)垃圾回收管理:自动侦测被程序保留但已不再使用的存储器空间并释放。
     保守(conservative)GC:没有分别严格管理指标器和数字值之存储器回收管理。此方法是如果它可以成为指标,那就以指标来看待它,即使它可能是个数值。此方法防止对象被意外回收,但它也无法释放出可能的存储器。
     虽然精确GC本身就是高效率的,但以精确GC为基础的高级算法,如分代(Generational)GC、复制(copy)GC以及标记和精简处理(mark-and-compact processing)等在性能上有明显的改善。分代(Generational)GC借由分开管理【年青分代(Young Generational)】对象(经常收集)和【旧分代(Old Generational)】对象(相对长寿的对象)而提升了GC效率。
     V8使用了分代(Generational)GC,在新分代(Generational)处理上使用轻度(light-load)复制GC,而在旧GC上使用标记和精简GC,因为它须在内存空间内移动对象。这很难在保守GC中执行。在对象的复制中,压缩(compaction)(在硬盘方面称为defrag)和类似动作时,对象的地址会改变,且基于这个原因,最普遍的方法是用【句柄】(handles)间接地引用地址。然而,V8不使用句柄(handles),而是重写该对象引用的所有数据。不使用句柄(handles)会使实现更困难,但却能改善性能,因为少了间接引用。Java VM HotSpot也使用相同的技术。

3) 内嵌缓存:js中不可用,V8使用隐藏类技巧
     
     V8目前可以针对x86和ARM架构产生适合的机器语言,虽然没采用C++或Java中传统的优化方式,V8还是有动态语言与生俱来的速度。
     
     其中一项良好范例是内嵌缓存(inline cache),这项技巧可以避免方法呼叫和属性存取时的哈希表搜寻。它可以立即缓存之前的搜寻结果,因此称为【内嵌】。人们知道此技术已有一段时间,已经被应用在Smalltalk、Java和Ruby等语言中。
     
     内嵌缓存假设对象都有类型之分,但在js中却没有,直到V8出现,而这就是为什么已签订js引擎都没有内嵌缓存的原因。
     为了突破此限制,v8在执行时就分析程序操作,并利用【隐藏类】(hidden classes)为对象指定暂时的类。有了隐藏类,即使是js也可以使用内嵌缓存。但是这些类是提升执行速度之技巧,不是语言规范的延伸。所以它们无法在JS代码中引用。

     其它的JavaScript引擎和V8不同,它们将对象属性储存在哈希表中,但V8则将它们储存在数组中。位移信息-指定个别属性在数组中的位置-是储存在隐藏类的哈希表中。同一隐藏类的对象具有相同的属性名称。如果知道对象类,那么就可以利用位移依数组操作存取属性。这比搜寻哈希表快许多。

     然而,在js等动态语言中,很难事先知道对象类型。例如,对象类型p和q呼叫lengthSquared()函数。对象类型p和q的属性不同,隐藏类也不同,因此无法判定lengthSquared()函数代码的参数(arguments)类型。

     若要读取函数中的对象属性,必须先检查对象的隐藏类,并又搜寻类的哈希表,以找出该属性的位移。然后利用位移存取数组。尽管是在数组中存取属性,要先搜寻哈希表的需求就毁掉了使用数组的优点。
     
     然而,从不同的观点来看,情况有所不同。在实际的程序中,依赖代码执行判断类型的情况并不多。例如,下图lengthSquared()函数甚至假设大部分通过成为参数的值,都是Point类对象,而一般而言这是正确的。

     function lengthSquared(p) {
          return p.x * p.x + p.y * p.y;
     }
     function LabeledLocation(name, x, y) {
          this.name = name;
          this.x = x;
          this.y = y;
     }
     var p = new Point(10, 20);
     var q = new LabeledLocation("hello", 10, 20);
     var plen = lengthSquared(p);
     var qlen = lengthSquared(q); 

     在执行之前根本无法判断参数是Point型或者lengthSqurared()函数的LabeledLocation型。

     内嵌缓存是一项加速技术,此设计是为了利用程序中局部(local)类别的方法。若要程序化的属性存取,V8会产生一个指令串来搜寻隐藏类列表,此代码称为premonomorphic stub。此stub是为了在函数存取属性。Premonomorphic stub拥有两个信息:搜寻用的隐藏类,以及取自隐藏的位移。最后会产生新代码以缓存此信息。

     Object* find_x_for_p_premorphic(Object* p) {
          Class* klass = p->get_class();
          int offset = klass->lookup_offset("x");
          update_cache(klass, offset);
          return p->properties[offset];
     }

在伪代码(pseudocode)中的premonomorphic stub从隐藏类中取得属性位移。
lengthSquared()函数


     
   
permonomorphic stub呼叫函数中的属性时会呼叫premonomorphic stub。

     Object* find_x_for_p_monomorphic(Object* p) {
          if (CACHED_KLASS == p->get_class()) {
               return p->properties[CACHED_OFFSET];
          } else {
               return lookup_property_on_monomorphic(p, "x");
          }
     }

伪代码的monomorphic stub处理直接嵌入代码中的位移是用来存取属性的常数。
     在搜寻表格之前,带有属性对象的隐藏类会与缓存隐藏类比较。如果相符就不需要再搜寻,且可以使用缓存的位移来存取属性。如果隐藏类不相符,就透过隐藏类哈希表以一般方式判断位移。

     新产生的代码被称为monomorphic stub。【内嵌】这个字的意思是查询隐藏类所需的位移,是以立即可用的形式嵌入在所产生的代码中。当第一次叫出monomorphic stub时,它会将功能从pre-monomorphic stub地址中所叫出的第一个地址重写成monomorphic stub地址。自此,使用高速的monomorphic stub,单靠类比较和数组存取就可以处理属性存取。




     当呼叫monomorphic stub时,它会将功能从premonomorphic stub地址中叫出的第一个地址,重写成monomorphic stub地址。

     如果只有一个具有属性的对象,monomorphic stub的效率就会很高。然而,如果类型愈多,缓存失误就会更频繁,进而降低monomorphic stub的效率。

     当缓存失误时,V8借由产生另一个称为megamorphic stub的代码来解决。与个别类对应的monomorphic stub都写在哈希表中,其在执行时搜寻和叫出stub。如果没有类型对应的monomorphic stub时,就会从类型哈希表中搜寻位移。

     Object* find_x_for_p_megamorphic(Object* p) {
          Class* klass = p->get_class();
          // 内嵌处理实际的搜寻
          Stub* stub = klass->lookup_cached_stub("x");
          if (NULL != stub) {
               return (*stub)(p);
          } else {
               return lookup_property_on_megamorphic(p, "x);
          }
     }
     伪代码中的Megamorphic stub处理与类型对应的monomorphic stub事先储存在哈希表中,并在执行时被搜寻和叫出。如果无法找到对应的monomorphic stub,就会在类型哈希表中搜寻位移。

     当monomorphic stub发生缓存失误时,monomorphic stub会将功能从monomorphic stub地址叫出的第一个地址以megamorphic stub地址重写。在代码搜寻方面,megamorphic stub的性能比monomorphic stub低,但是megamorphic代码却比使用缓存更新、代码生成及其他辅助处理的premonomorphic stubs快许多。

涵盖多种类的内嵌缓存称为多形态内嵌缓存(polymorphic inline cache)。V8内嵌缓存系统被用来呼叫方法以及存储属性。


4) 隐藏类 存储类型转换信息

     隐藏类为没有类之分的js语言规范带来有趣的挑战,同时也是V8用来提升速度最独特的技巧。它们值得更深入的探究。
     
     在V8中建立类有两个主要的理由,即(1)将属性名称相同的对象归类,及(2)识别属性名称不同的对象。前一类中的对象有完全相同的对象描述,而这可以加速属性存取。

     在V8,符合归类条件的类会配置在各种js对象上。对象引用所配置的类。然而这些类只存在于V8作为方便之用,所以它们是【隐藏】的。


     

     如果对象的描述是相同的,那么隐藏类也会相同。在此范例中,对象p和q都属于相同的隐藏类
     
     上面提到随时可以在js中新增或删除属性。然而当此事发生时,会毁灭归类条件(归纳名称相同的属性)。V8借由建立属性变化所需的新类来解决。属性改变的对象透过一个称为【类型转换(class transition)】的程序纳入新级别中。

     

配置新类:类型转换

属性改变的对象会被归为新类。当对象p增加了新属性z时,对象p就会被归为新类。
V8将变换信息存储在类内,来解决此问题。当隐藏类Point有x和y属性时,新属性z就会新增至Point级的对象p中。当新属性z加到对象p时,V8会将【新增属性z,建立Point2类】的信息储存在Point级的内部表格中。

在类中存储类变换信息当在对象p中加入新属性z时,V8会在Point类内的表格上记录【加入属性z,建立类Point2】(步骤1)。当同一Point类的对象q加入属性z时,V8会先搜寻Point类表。如果它发现了Point2类已加入属性z时,就会将对象q设定在Point2类(步骤2)。如下图:




当新属性z新增至也是Point级的对象q时,V8会先搜寻Point级的表格,并发现Point2级已加入属性z。在表格中找到类时,对象q就会被设定至该类(Point2),而不建立新类(步骤2),这就达到了归纳属性名称相同的对象目的。

     然而此方法,意味着与隐藏类对应的空对象会有庞大的转换表格。V8透过为各个建构函数建立隐藏类来处理。如果建构函数不同,就算对象的陈述(layout)完全相同,也会为它建立一个新的隐藏类。

5)机器语言的特性

如以上所述,V8在设计时使用了例如内嵌缓存等,来达到动态语言中天生的速度。创建使用于内嵌缓存之stub的机器语言生成模块密切地与JIT编译器连结。

一些经常使用的方法也被写成机器语言以达到与内嵌拓展相同的效果,使它们成为【内在】的。V8原始码列出了内在转换的候选名单。

V8所含的shell程序可以用来检查V8所产生的机器语言。所产生的指令串可以和V8代码比较,以便显出它的特性。

例如,在执行图14a所示的js函数时,就会产生一个如下图所示的x86机器语言指令串。此函数在第39个指令中被呼叫,是个[n+one]加法。在js中,[+]操作数指示数字变量的加法,以及字符串的连续性。编译器不是产生代码来判决这是哪一种,而是呼叫函数来负责判断。



V8从js代码产生的机器语言加法处理被转换成函数呼叫的机器语言(a, b)。

如果上图的函数稍做更改,那么函数呼叫就会消失,但会有个加法指令及分支指令(JNZ的若不是零就跳出)。当使用整数作为[+]操作符的操作数,V8编译器在不呼叫函数下会产生一个有【加法】指令的指令串。如果发现操作数(在此为[n])成了Number对象或String对象等的指标(pointer),就会叫出函数。【加法】只会发生在当两个【+】运算的操作数都是整数时。在这种情况下,因为可以跳过函数呼叫所以执行就会比较快。




    小幅修改js后,产生的机器语言。

     此外,0x2会加上【加法】指令,因为为最低有效位(least significant bit, LSB)被用来区别整数(0)和指标(1)。加0x2(二进制中的十)就如同在该值加上1,LSB除外。在jo指令的溢位(overflow)处理中,利用测试和jnz指令来判定指标,跳到下游处理。
     这类的窍门在编译器中到处都有。然而,产生机器代码也透露了编译器的限制。具传统最佳化的编译器可以针对上面两图产生完全一样的机器语言,这是由于常数进位关系。然而V8编译器是在抽象语法树(abstract syntax tree)单元中产生代码,因此在处理延伸多个节点时就没有最佳化。这在大量的push和pop指令也非常明显。




上图显示了C语言产生的机器语言,由于C和js之间语言规范不同,因此所产生的机器语言是不同,和编译器性能无关。
c编译器从c代码所产生的机器语言比V8所产生的干净许多(a, b),大部分是因为c和js语言规范差异导致。
注1:当溢位信号出现时,jo指令会跳至特定的地址。测试指令将逻辑AND结果反映成零和符号指标等。除非零信号出现,否则jnz指令会跳至特定的位址。

Abstract syntax tree抽象语法树:在树状架构中代表程序架构的数据。

七、熟悉js的面向对象

     js没有类,但为了让熟悉使用类(面向对象的代码)更方便,可以使用new操作数来建立对象,就像在java一样,在new操作数之后会定义一个特别的constructor构造函数。
     然而,即使没有构造函数,也可以建立对象和设定属性的,js对象的属性和方法等随时可以新增和删除。
     除了用点标记(dot notation)存取js属性以外,也可以使用括号,建议散列(hashing)存取或是以变量特定属性名称字符串。从这些范例中明确显示js对象设计是为了使用哈希表。

a) 定义构造函数[point]
function Point(x, y) {
     // this是指它自己
     this.x = x;
     this.y = y;
}

b)当增加新的及呼叫构建函数时所建立的对象
var p = new Point(10, 20);

c) 没有构建函数也可以建立对象
var p = { x: 10, y: 20 };

d) 可以自由地在对象上新增属性
p.z = 30;

e) 使用点标记存取属性
var y = p.y;

f) 使用括号之散列(hashing)存取
var y = p["y"];

g) 也可以使用变量进行散列(hashing)存取
var name = "y";
p[name];




0 0
原创粉丝点击