资讯专栏INFORMATION COLUMN

javascript引擎——V8

luoyibu / 2281人阅读

摘要:类将源代码解释并构建成抽象语法树,使用类来创建它们,并使用类来分配内存。类抽象语法树的访问者类,主要用来遍历抽象语法树。在该函数中,先使用类来生成抽象语法树再使用类来生成本地代码。

通过上一篇文章,我们知道了JavaScript引擎是执行JavaScript代码的程序或解释器,了解了JavaScript引擎的基本工作原理。我们经常听说的JavaScript引擎就是V8引擎,这篇文章我们就来认识一下V8引擎,我们先来看一下除了V8引擎,还有哪些JS引擎:

V8 开源
由Google开发,用C++编写。V8 最早被开发用以嵌入到 Google 的开源浏览器 Chrome 中,但是 V8 是一个可以独立的模块,完全可以嵌入您自己的应用,著名的 Node.js( 一个异步的服务器框架,可以在服务端使用 JavaScript 写出高效的网络服务器 ) 就是基于 V8 引擎的。

Rhino开源
 由Mozilla基金所管理,完全用Java开发

JavaScriptCore 开源
由苹果公司为Safari开发

SpiderMonkey
第一个JavaScript引擎,最早用在Netscape Navigator上,现在用在Firefox上

KJS
KDE的引擎,最初由Harri Porten为KDE项目的Konqueror网页浏览器所开发

Chakra(JScript9)
 Internet Explorer 浏览器

Chakra(JavaScript)

Microsoft Edge

Nashorn
OpenJDK开源项目的一部分,用的是Oracle Java语言和工具组

JerryScript
用于物联网的轻量级引擎

在这些项目中,V8引擎因其在性能上的突出表现,倍受大家的关注,所以我们也以介绍V8引擎为主。V8是Google开源的高性能JavaScript引擎,用C++编写。它用于谷歌浏览器,谷歌的开源浏览器,以及Node.js等等。

速度是V8追求的主要设计目标之一,在一些性能测试中,V8比IE的JScript,Firefox中的SpiderMonkey以及Safari中的JavaScriptCore要快上数倍。相比其他的JavaScript引擎转化成字节码或解释执行,V8将其编译成本地代码,并且使用了如隐类型,内联缓存等方法来提高性能。

http://kourge.net/node/122

V8按照ECMA-262第5版中的规定实施ECMAScript,支持众多操作系统,如windows、linux、android等,也支持其他硬件架构,如IA32,X64,ARM等,具有很好的可移植和跨平台特性。

V8的工作过程

V8工作的整个过程与Java有些类似,大致分成两个阶段:第一是编译,第二是运行。与C++直接编译成本地代码不同的是,V8只有在函数调用时才会编译成本地代码,这样就提高了响应时间减少了时间开销。

图片来源《WebKit技术内幕》

在V8引擎中,源代码先通过解析器转变成抽象语法树,这点同JavaScriptCore引擎一样,不同于JavaScriptCore引擎,V8引擎中并不将抽象语法树转变成字节码或者其他中间表示,而是通过JIT全代码生成器(full code generator)从抽象语法树直接生成本地代码,这样做可以减少抽象语法树到字节码的转换时间,提高代码的执行速度,但也是因为缺少了转换为字节码这一中间过程,也就减少了优化中间代码的机会。

下面来看一下V8引擎编译JavaScript生成本地代码使用了哪些主要类:

Script类:表示是JavaScript代码,既包含源代码,又包含编译之后生成的本地代码,所以它既是编译入口,又是运行入口

Compiler类:编译器类,辅助Script类来编译生成代码,它主要起一个协调者的作用,会调用解释器(Parser)来生成抽象语法树和全代码生成器,来为抽象 语法树生成本地代码。

Parser类:将源代码解释并构建成抽象语法树,使用AstNode类来创建它们,并使用Zone类来分配内存。

AstNode类:抽象语法树节点类,是其他所有节点的基类,它包含非常多的子类,后面会针对不同的子类生成不同的本地代码。

AstVisitor类:抽象语法树的访问者类,主要用来遍历抽象语法树。

FullCodeGenerator:AstVisitor类的子类,通过遍历抽象语法树来为JavaScrit生成本地代码。

图片来源《WebKit技术内幕》

JavaScript代码编译的过程大致为:Script类调用Compiler类的Compile函数生成本地代码。在该函数中,先使用Parser类来生成抽象语法树;再使用FullCodeGenerator类来生成本地代码。

图片来源《WebKit技术内幕》

本地代码与具体的硬件平台密切相关,FullCodeGenerator使用多个后端来生成与平台相匹配的本地汇编代码。由于FullCodeGenerator通过遍历AST来为每个节点生成相应的汇编代码,缺失了全局视图,节点之间的优化也就无从谈起。

JavaScript代码编译之前需要构建一个运行环境,所以JavaScript代码编译之前,V8引擎会构建众多对象并加载一些内置的库(如Math库)。再次强调一下,在JavaScript源码中,并非所有的函数都被编译生成本地代码,而是延时编译,在调用时才会编译。

由于V8缺少生成字节码(中间表示)这一环节,缺少必要的优化,为了性能上的考虑,V8会在生成本地代码后,使用数据分析器(Profiler)采集一些信息,然后根据这些信息对本地代码进行优化,生成更高效率的本地代码,这是一个逐步改进的过程。同时,当发现优化后的代码性能并没有提高甚至还有所降低时,V8将退回到原来的代码。这些都是在运行阶段用涉及到的技术。

现在我们来看一下运行阶段使用到的类:

Script: 表示是JavaScript代码,既包含源代码,又包含编译之后生成的本地代码,所以它既是编译入口,又是运行入口

Execution: 运行代码的辅助类,包含一些重要的函数,例如call,它辅助进入和执行Script中的本地代码

JSFunction: 需要执行的JavaScript函数表示类

Runtime:运行这些本地代码的辅助类,它的功能主要是提供运行时各种各样的辅助函数,包括但是不限于属性访问、类型转换、编译、算数、位操作、比较、正则表达式等

Heap:运行本地代码需要使用内存堆

MarkCompactCollector:垃圾回收机制的主要实现类,用来标记(Mark)、清除(Sweep)和整理(Compact)等基本的垃圾回收过程

SweeperThread:负责垃圾回收的线程


图片来源《WebKit技术内幕》

首先,当某个JavaScript函数被调用时,使用编译阶段的类和操作编译生成本地代码。具体的工作方式是V8查找函数是否已经生成本地代码,如果已经生成,那么直接使用这个函数。否则,V8引擎会触发生成本地代码,这样的工作方式可以节约时间,减少去处理那些使用不到的代码的时间。其次,执行编译后的代码为JavaScript构建JS对象,这需要Runtime类来辅助创建对象,并需要从Heap类分配内容。再次,借助Runtime类中的辅助函数来完成一些功能,如属性访问,类型转换等。最后,将不用的空间进行标记清除和垃圾回收。


图片来源《WebKit技术内幕》

V8特性简介 一. 优化回滚

FullCodeGenerator编译器基于抽象语法树直接生成本地代码,没有中间表示层,所以很多时候没有经过很好的优化。JavaScript引擎性能之争非常激烈,没有经过优化的代码导致该引擎在性能上同有特别大的突破,而其他引擎都在进度,有鉴于此,在2010年,V8引入了新的编译器,这就是Crankshaft编译器,它主要针对那些热点函数进行优化。该编译器基于JavaScript源代码开始分析,而不是本地代码,同时构建Hydtogen图并基于此来进行优化分析。

FullCodeGenerator是一个简单且快的编译器,生成未优化的本地代码,运行起来很慢;Crankshaft是一个相对慢的编译器,生成高度优化的代码。由FullCodeGenerator生成的未优化代码Crankshaft优化代码替换,传送门。

Crankshaft编译器为了性能考虑,通常会做出比较乐观和大胆的预测,那就是编译器认为这些代码比较稳定,变量类型不会发生改变,所以能够生成高效的本地代码。但是在实际执行过程中,因为JavaScript弱类型语言的特性,变量类型有可能会改变,在这种情况下,V8会将该编译器做的错误优化回滚到之前的一般情况,这个过程称为优化回滚。

V8并不只是第一次执行一个JavaScript函数时才编译它;同一个JavaScript函数可以被这些JIT编译器多次编译。

基本流程是:

    [JavaScript函数] ->
        第一次被调用 -> Full Code -> [初级编译后的代码]
         足够热之后 -> Crankshaft(Optimizing Compiler) -> [优化编译后的代码]
如果优化的代码需要去优化(优化回滚) -> deoptimize -> 回到[初级编译后的代码]
    ... 周而复始 ...

示例如下:

var counter = 0;
function test(x,y){
    counter ++;
    if(counter < 10000000){
        // do something
        return 123;
    }
    var unknown = new Date();
    console.log(unknown);
}

函数test被调用多次后,V8引擎可能会触发Crankshaft编译器来生成优化的代码,优化的代码认为示例代码的类型等信息都已经被获知,但事实上还未真正执行到new Date()这个地方,并未获取unknown这个变量的类型,V8只得将该部分的代码进行回滚。优化回滚是一个很费时的操作,所以在写代码的过程中,尽量不要触发这个过程。

二. 隐类型和内嵌缓存

我们都知道JavaScript属于动态类型语言,只有在运行时才能确定变量的类型,在运行时计算和决定类型,会带来严重的性能损失,这也就导致了JavaScript语言的运行效率比C++或Java都要低很多。

主要体现在以下几个部分:

编译确定位置:
C++在编译阶段对象的属性和偏移信息都计算完成;而这些信息JavaScript只有在执行阶段才可以确定

偏移信息共享:
C++属于静态类型语言,不能在执行时动态改变类型,这些对象都是共享偏移信息的。访问对象时就按编译时的偏移量即可;JavaScript每个对象都是自描述,属性和位置偏移信息都包含在自身的结构中。

一个简单的C++函数:

 class Class1 {
     int x;
     int y;
 }
 int add(Class1 a, Class1 b){
     return a.x*a.y + b.x*b.y;
 }

示例代码中的类型和对象的结构表示,如下图:

图片来源《WebKit技术内幕》

一个简单的JavaScript函数:

function add(a,b){
    return a.x*a.y + b.x*b.y; // 这里对象a和b的类型未知
}
var a = {x:3.3,y:5.5};
var b = {x:4.4,y:6.6};

示例代码中对象a和b的结构表示,如下图:

图片来源《WebKit技术内幕》

偏移信息查找:
C++中查找偏移地址很简单,都是在编译代码时,对使用到某类型的成员变量直接设置偏移量。而对于JavaScript,使用到一个对象则需要通过属性名匹配才能查找到对应的值

因为对象属性的访问非常普遍而且次数非常频繁,像C++这种通过偏移量来访问值使用少数两个汇编指定就能完成,但是Javascript这种通过属性名来匹配对于性能造成的影响可能会多很多倍,因为属性名匹配需要特别长的时间,而且额外浪费很多内存空间。

有方法解决这一问题么?答案是肯定的。下面我们就来看一下V8引擎是如何解决这一问题的。虽然JavaScript语言没有类型的定义,但是V8使用类和偏移位置思想,将本来需要通过字符串匹配来查找属性值的算法改进为使用类似C++编译器的偏移位置的机制来实现。这就是隐藏类(Hidden Class)

JavaScript对象的实现在V8中包含3个成员,第一个是隐藏类的指针,这是V8为JavaScript对象创建的隐藏类。第二个指向这个对象包含的属性值。第三个指向这个对象包含的元素。


图片来源《WebKit技术内幕》

隐藏类将对象划分成不同的组,对于相同的组,也就是该组内的对象拥有相同的属性名和属性值的情况,将这些属性名和对应的偏移位置保存在一个隐藏类中,组内的所有对象共享该信息。同时,也可以识别属性不同的对象。

V8引擎的发展历史

2008年9月,V8的第一个版本随着Chrome的第一版发布。

2010年12月,官方公布V8的名为Crankshaft的优化编译器,与原来的Full Compiler一起工作,声称较2008年版本提高50%性能。

2015年7月7日,官方公布又一个新的中为TurBoFan的优化编译器,主要提供ES6的新语法,以及提高性能。并表明该编译器最终目标是全部替代Crankshaft编译器。

2015年7月17日,官方公布集成了TurboFan的V8版本(v4.5)

2015年8月28日,V8发布v4.6版本

2016年3月15日,V8发布v5.0版本

2016年7月18日,V8发布v5.3版本,新增名为Ignition的解析器(Interpreter),跟原有的优化编译器(Crankshaft and TurboFan)进行串联工作,提供了更加优化的内存使用方案,主要针对于低内存的Android设备,并称在未来会普及到全平台。

2016年9月9日,V8发布v5.4版本

2016年10月24日,V8发布v5.5版本,在5.5版本中开始支持ES7异步函数,这使得编写使用和创建Promise的代码变得更加容易。

2016年12月2日,V8发布v5.6版本,从5.6版本开始,V8可以优化整个JavaScript语言。而且,许多语言功能都是通过V8中的新优化管道发送的。该管道使用V8的Ignition解释器作为基准,并使用V8更强大的TurboFan优化编译器优化经常执行的方法。新的流水线激活了新的语言功能(例如ES2015和ES2016规范中的许多新功能)或Crankshaft(V8的“经典”优化编译器)无法优化某种方法(例如try-catch,with)的情况。

2017年2月6日,V8发布v5.7版本

2017年3月20日,V8发布v5.8版本

2017年4月27日,V8发布v5.9版本,V8 5.9将成为默认启用Ignition + Turbofan的第一个版本。一般来说,这种交换机应该可以降低内存消耗,并且可以更快地启动Web应用程序。

2017年6月9日,V8发布v6.0版本,V8 6.0引入了对SharedArrayBuffer的支持,SharedArrayBuffer是一种在JavaScript工作人员之间共享内存并在工作人员之间同步控制流的低级机制。 SharedArrayBuffers为JavaScript提供了对共享内存,原子和futex的访问。 SharedArrayBuffers还解锁了通过asm.js或WebAssembly将线程化应用程序移植到Web的功能。

2017年8月3日,V8发布v6.1版本

2017年9月11日,V8发布v6.2版本

2017年10月25日,V8发布v6.3版本,改进了速度和内存消耗,详细

2017年12月19日,V8发布v6.4版本,提升了速度和优化内存消耗,详细

2018年2月1日,V8发布v6.5版本,编译速度显著提升,详细

2018年3月27日,V8发布v6.6版本,异步性能大幅提升,详细

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/94202.html

相关文章

  • JavaScript工作原理(二):V8引擎和5招高效代码

    摘要:引擎可以用标准解释器或即时编译器来实现,即时编译器以某种形式将代码编译为字节码。这里的主要区别在于不生成字节码或任何中间代码。请注意,不使用中间字节码表示法,不需要解释器。这允许在正常执行期间非常短的暂停。 本系列的第一篇文章重点介绍了引擎,运行时和调用栈的概述。第二篇文章将深入V8的JavaScript引擎的内部。我们还会提供一些关于如何编写更好的JavaScript代码的技巧。 概...

    leone 评论0 收藏0
  • JavaScript是如何工作的:深入V8引擎&编写优化代码的5个技巧

    摘要:第二篇文章将深入谷歌的引擎的内部。引擎可以实现为标准解释器,或者以某种形式将编译为字节码的即时编译器。这个引擎是在谷歌中使用的,但是,与其他引擎不同的是也用于流行的。一种更复杂的优化编译器,生成高度优化的代码。不是唯一能够做到的引擎。 本系列的 第一篇文章 主要介绍引擎、运行时和调用堆栈。第二篇文章将深入谷歌 V8 的JavaScript引擎的内部。 想阅读更多优质文章请猛戳GitHu...

    Turbo 评论0 收藏0
  • JavaScript是如何工作的:深入V8引擎&编写优化代码的5个技巧

    摘要:第二篇文章将深入谷歌的引擎的内部。引擎可以实现为标准解释器,或者以某种形式将编译为字节码的即时编译器。这个引擎是在谷歌中使用的,但是,与其他引擎不同的是也用于流行的。一种更复杂的优化编译器,生成高度优化的代码。不是唯一能够做到的引擎。 本系列的 第一篇文章 主要介绍引擎、运行时和调用堆栈。第二篇文章将深入谷歌 V8 的JavaScript引擎的内部。 想阅读更多优质文章请猛戳GitHu...

    DevWiki 评论0 收藏0
  • [译文] JavaScript工作原理:V8引擎内部+5条优化代码的窍门

    摘要:本文将会深入分析的引擎的内部实现。该引擎使用在谷歌浏览器内部。同其他现代引擎如或所做的一样,通过实现即时编译器在执行时将代码编译成机器代码。这可使正常执行期间只发生相当短的暂停。 原文 How JavaScript works: inside the V8 engine + 5 tips on how to write optimized code 几周前我们开始了一个系列博文旨在深入...

    dreamans 评论0 收藏0
  • JavaScript 工作原理之二-如何在 V8 引擎中书写最优代码的 5 条小技巧(译)

    摘要:本章将会深入谷歌引擎的内部结构。一个引擎可以用标准解释程序或者即时编译器来实现,即时编译器即以某种形式把解释为字节码。引擎的由来引擎是由谷歌开源并以语言编写。注意到没有使用中间字节码来表示,这样就不需要解释器了。 原文请查阅这里,略有删减。 本系列持续更新中,Github 地址请查阅这里。 这是 JavaScript 工作原理的第二章。 本章将会深入谷歌 V8 引擎的内部结构。我们也会...

    PingCAP 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<