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带来的优化的主要内容,如果未能解决你的问题,请参考以下文章