V8引擎对JS带来的优化

Posted 不作声

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了V8引擎对JS带来的优化相关的知识,希望对你有一定的参考价值。

两类型语言

一、编译型语言:在程序执行之前必须进行专门的编译过程,如C、C++、Java等。

编译型语言有以下特点:

  • 只需编译一次****就可以把源代码编译成机器语言,后边的执行无需重新编译,直接使用之前的编译结果就可以,因此执行效率比较高**。
  • 程序执行效率比较高,但比较依赖编译器,因此跨平台性差一些
  • 不同平台对编译器影响很大。
    • 16位系统下int是2个字节(16位),而32位系统下int占4个字节。
    • 32位系统下long类型占4字节,而64位下long类型占8个字节。

二、解释型语言:支持动态类型,弱类型,在程序运行的时候才进行编译,而编译前需要确定变量的类型,效率比较低,对不同系统平台有较大的兼容性。

解释型语言有以下特点:

  • 源代码不能直接编译成机器语言,而是先翻译成中间代码,再由解释器对中间代码进行解释运行
  • 程序不需要编译,程序在运行的时候才需要编译成机器语言,每执行一次都要编译一次
  • 运行效率一般相对比较低,依赖解释器,跨平台性好。

三、比较:

  • 一般,编译型语言的运行效率比解释型语言更高,但不能一概而论。
  • 编译型语言的跨平台特性比解释型语言差一些。

随着web相关技术的发展,javascript所要承担的工作也越来越多了,早就超越了“表单验证”的范畴,这就

更需要快速的解析和执行JavaScript脚本。V8引擎就是为解决这一引擎而生,在node中也是采用该引擎来解析JavaScript。

V8引擎

V8引擎使用C++开发,在运行JavaScript之前,相比其它的JavaScript的引擎转换成字节码或解释行,V8将其编译成原生机器码,并且使用了如内联缓存等方法来提高性能。有了这些功能以后,JavaScript程序在V8引擎下的运行速度媲美二进制程序。V8支持众多操作系统,如windows、Linux、android等,也支持其他硬件架构,如ARM,X64等。具有很好的可移植性和跨平台特性。

数据表示

JavaScript是一种动态类型语言,在编译时并不能准确知道变量的类型,只可以在运行时确定,这就不像C++或者Java等静态类型语言,在编译时就可以确切的知道变量的类型。在运行时计算和决定变量的类型,会严重影响语言性能,这也就是JavaScript运行效率比C++或是Java低很多的原因之一。

在C++中,源代码需要经过编译才能执行,在生成本地代码的过程中,变量的地址和类型就已经确定,运行本地代码时利用数组和位移就可以存取变量和方法的地址,不需要再进行额外的查找,几个机器指令即可完成,节省了确定类型和地址的时间。JS是无类型语言,无法在执行时就知道变量的类型和地址,所以需要确定。

JS和C++的几个区别:

  • 编译确定位置。C++编译阶段确定位置偏移信息,在执行时直接存取;JS在执行阶段确定,而且执行期间可以修改对象属性。
  • 偏移信息共享。C++有类型定义,执行时不能动态改变,可共享偏移信息;JS每个对象都是自描述,属性和位置偏移信息都包含在自身结构中
  • 偏移信息查找。C++查找偏移地址很简单,在编译代码阶段,对使用的某类型成员变量直接设置编译位置;JS中使用一个对象,需要通过属性名匹配才能找到相应的值,需要更多的操作。

在代码执行过程中,变量的存取是非常普遍和频繁的,通过偏移量来存取,使用少数汇编指令就能完成,如果通过属性名匹配则需要更多的汇编指令,也需要更多的内存空间。

在JS中,除了boolean,number,string,null,undefined五种基本类型,其他的数据都是对象,V8使用一种特殊的方式来表示他们,进而优化JS的内部表达问题。

在V8中,数据的内部表示由数据的实际内容和数据的句柄构成。数据的实际内容是变长的,类型也是不同的;句柄大小固定,包含指向数据的指针。这种设计可以方便V8进行垃圾回收和移动数据内容,相比于直接使用指针,使用者使用句柄,只需要修改句柄中的指针,而指针的修改对使用者是透明的。

除少数数据(如整型数据)由句柄本身存储外,其他内容限于句柄大小和变长等原因,都存储在堆中。整数直接从value中取值,然后使用一个指针指向它,可以减少内存的占用并提高访问速度。

JavaScript对象在V8中的实现包含三部分:隐藏类指针,V8为JS对象创建的隐藏类;属性值指针,指向该对象的属性值;元素值指针,指向该对象的属性。

工作过程

在V8引擎中,JavaScript相关代码并非是一下完成编译的,而是在某些代码需要执行时才会进行编译,这就提高了响应时间,减少了时间开销。源代码先被解析成抽象语法树(AST),然后使用解释器或者编译器转换为Bytecode或者Machine code这种本地可执行代码。

编译阶段

JavaScript代码的编译过程:

1、Script类调用Compiler类的Compile函数为其生成本地代码;

2、Compile函数先试用Parser类生成AST,在使用FullCodeGenerator类生成本地代码;

3、本地代码与具体的硬件平台密切相关,FullCodeGenerator使用多个后端来生成与平台相匹配的本地汇编语言。

4、由于FullCodeGenerator通过遍历AST来为每个节点生成相应的汇编代码,缺失了全局视图,节点之间 优化也就无从谈起。

在执行编译之前,V8会构建众多的全局对象并加载一些内置的库来构建一个运行环境。而且在JavaScript源代码中,并非所有的函数都被编译成本地代码,而是延迟编译,在调用时才会编译。

运行阶段

为了性能提升,V8在生成本地代码后,使用数据分析器(profiler)采集一些信息,然后根据这些数据将本地代码进行优化,生成更高效的本地代码,这是一个逐步改进的过程。当发现优化后的代码还不如未优化的代码,V8会退回到原来的代码,也就是优化回滚。

运行阶段过程描述:

1、先根据需要编译和生成这些本地代码;

2、在V8中,函数是一个基本单位,当某个JS函数被调用时,V8会查找该函数是否已生成本地代码,如果已经生成,则直接调用该函数。否则,就生成该函数的本地代码。

3、执行编译后的代码为JavaScript构建JS对象,这需要Runtime类来辅助创建对象,并需要从Heap类分配内存。

4、借助Runtime类中的辅助函数来完成一些功能,如属性访问等。最后,将不用的空间进行标记清除和垃圾回收。

优化回滚

V8中有一个Ignition字节码编辑器,TurBoFan和Ignition结合起来共同完成JavaScript的编译,消除了CranShaft这个旧的编辑器,并让新的Ignition直接从字节码来优化代码,并当需要反优化的时候就直接反优化到字节码,而不需要考虑到JS源码。

隐藏类

V8借用了类和偏移位置的思想,将本来通过属性名匹配来访问属性值的方法进行了改进,使用类似C++编译器的偏移位置机制来实现,这就是隐藏类。

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

内嵌缓存

正常访问对象属性的过程:首先获取隐藏类的地址,然后根据属性名查找偏移值,然后计算该属性的地址。如果将之前查询的结果缓存起来,可以供再次访问,这就是内嵌缓存。

内嵌内存的思路是,将初次查找的隐藏类和偏移值保存起来,当下次查找的时候,先比较当前对象是否为之前的隐藏类,如果是直接使用之前的缓存结果,减少再次查表的时间。但是如果一个对象有多个属性,缓存失误的概率就会提高,因为属性的类型变化后,对象的隐藏类也会变化,与之前的缓存不一致,需要重新使用之前的方法查找哈希表。

快照

V8引入了快照机制,将内置的对象和函数加载之后的内存保存并序列化。序列化以后的结果很容易反序列化,经过快照机制的启动时间可以缩减几毫秒。快照机制也可以将一些开发者认为需要的JS文件序列化来减少处理事件。

总结

随着V8引擎的发展,我们可以在编程中注意一些问题来做到性能优化:

  • 类型。一个函数应该使用比较少的数据类型;对于数组,应尽量存放相同类型的数据,这样就可以通过偏移位置来访问。
  • 数据表示。简单类型的数据直接保存在句柄中,可以减少寻址时间和内存占用,如果可以使用整数表示的,尽量不要使用浮点数。
  • 内存。对不再使用的对象设置为null或使用delete方法来删除。设置为null可以做到手动垃圾回收,使用delete会引发额外操作。
  • 优化回滚。在执行多次后,不要出现修改对象类型的语句,尽量不要触发优化回滚,否则会大幅度降低代码的性能。
  • 新机制。使用JS引擎或者渲染引擎提供的新机制和新接口提高性能。

技术图片

以上是关于V8引擎对JS带来的优化的主要内容,如果未能解决你的问题,请参考以下文章

Node.js背后的V8引擎优化技术

Node.js背后的V8引擎优化技术

Chrome V8系列--浅析Chrome V8引擎中的垃圾回收机制和内存泄露优化策略[转]

新 V8 即将推出和 Node.js

node.js

JavaScript性能优化2——浅谈JavaScriptV8引擎工作流程