V8引擎笔记整理(一)——JS编译方案发展简史

V8引擎笔记整理(一)

1 - 知名的js引擎有哪些?

  • V8-由谷歌开源的以 C++ 语言编写Google开源高性能JavaScript和WebAssembly引擎。它用于Chrome和Node.js等。它实现了ECMAScript和WebAssembly,并在Windows 7或更高版本,macOS 10.12+和使用x64,IA-32,ARM或MIPS处理器的Linux系统上运行。—— 简单来说,它是一个接收JavaScript代码,编译代码然后执行的C++程序,编译后的代码可以在多种操作系统多种处理器上运行。

    V8引擎的作用:

    ​ 1 - 编译和执行JS代码

    ​ 2 - 处理JS调用栈

    ​ 3 - 内存分配

    ​ 4 - 垃圾回收

  • Rhin-由 Mozilla 基金会主导,开源的,完全使用 Java 开发。

  • SpiderMonkey-初代 JavaScript 引擎,由在之前由网景浏览器提供技术支持,现在由 Firefox 使用。

  • JavaScriptCore-开源,以 Nitro 的名称来推广,并由苹果为 Safari 开发。

  • KJS-KDE 引擎,起先是由 Harri Porten 为 KDE 工程的 Konqueror 浏览器所开发。

  • Chakra (JScript9)-IE

  • Chakra (JavaScript)-Microsoft Edge

  • Nashorn-作为 OpenJDK 的一部分来开源,由 Oracle Java 语言和 Tool Group 编写。

  • JerryScript-一款轻量级的物联网引擎。

2 - V8引擎编译方案发展简史

2.1 - 编译工具简介

  • parser(解析器):将JS代码解析成AST语法树
  • interpreter(解释器):将AST解释成bytecode字节码,同时也具有解释执行bytecode的能力
  • compiler(编译器):编译机器码

2.2 - V8(5.9版)之前——Full-codegen编译器和Crankshaft编译器

​ 在5.9版本还没发布之前,V8并没有使用interpreter生成bytecode,而是使用了两个compiler——Full-codegen编译器Crankshaft编译器——直接将AST编译成机器码执行,执行流程如下:

ps:首先明白一件事情,JS是单线程语言的意思是JS只用一个调用栈,并不是说JS执行只用了一条线程。V8编译执行JS事,主线程做的确实是抓取JS代码,并编译执行,但同时也有多个独立的线程在做自己的工作。

  1. 有一个独立的线程使用parser解析器将JS代码解析成AST抽象语法树;
  2. 使用Full-codegen编译器直接将AST编译成机器码——这里的机器码是基准的未经优化的机器码,故Full-codegen编译器又称基准编译器;
  3. 代码运行一段时间后,V8引擎的分析器线程(一个用于性能检测的线程)收集了足够的数据帮助我们的Crankshaft编译器(又称优化编译器)(同时这里又是一个单独的线程)优化JS代码,优化方式稍后述明;
  4. 当然我们不能忘了内存管理的线程和GC的线程也在工作中
Crankshaft 代码优化方式简述:

​ 把 AST转化为一个被称为 Hydrogen 的高级静态单赋值并且试着优化这个 Hydrogen 图表,一旦优化了 Hydrogen 图表,Crankshaft 会把它降级为低级的展现叫做 Lithium。大多数 Lithium 的实现都是依赖于指定的架构的(寄存器分配发生在这一层)。

Hydrogen优化方法:

  1. 尽可能内联——尽可能多的把调用地址置换未被调用函数的函数体;
  2. 使用隐藏类——每当对象添加新的属性,使用转换路径来把旧的隐藏类更新为新的隐藏类。如果两个对象共享一个隐藏类并且两个对象添加了相同的属性,转换会保证两个对象收到相同的新的隐藏类并且所有的优化过的代码都会包含这些新的隐藏类;
  3. 内联缓存——V8 会维护一份传入最近调用方法作为参数的对象类型的缓存,然后使用这份信息假设在未来某个时候这个对象类型将会被传入这个方法。如果 V8 能够很好地预判即将传入方法的对象类型,它就可以绕过寻找如何访问对象属性的过程,代之以使用储存的来自之前查找到的对象隐藏类的信息。

​ 最后,Lithium会被编译成机器码,并进行OSR(Online Stack Replacement)——转换所有的上下文(包括堆栈和寄存器),这样就可以在执行的过程中切换到优化的版本代码——这里有被称为逆优化的安全防护,以防止当引擎所假设的事情没有发生的时候,可以进行逆向转换和把代码反转为未优化的代码。

优点:
  • 显而易见,由于省去了将AST解释成bytecode的时间,可以提高外部浏览器中JS执行的性能
缺点:
  • 生成的机器码会占用大量的内存,且有些代码只执行一次,没必要直接编译成机器码

    ​ The original motivation behind V8’s Ignition interpreter was to reduce memory consumption on mobile devices. Before Ignition, the code generated by V8’s Full-codegen baseline compiler typically occupied almost one third of the overall JavaScript heap in Chrome. That left less space for a web application’s actual data. When Ignition was enabled for Chrome M53 on Android devices with limited RAM, the memory footprint required for baseline, non-optimized JavaScript code shrank by a factor of nine on ARM64-based mobile devices.

    ​ ——《 Launching Ignition and TurboFan

  • 由于没有生成中间层bytecode字节码,故有些优化策略难以实现

    ​ Later the V8 team took advantage of the fact that Ignition’s bytecode can be used to generate optimized machine code with TurboFan directly rather than having to re-compile from source code as Crankshaft did. Ignition’s bytecode provides a cleaner and less error-prone baseline execution model in V8, simplifying the deoptimization mechanism that is a key feature of V8’s adaptive optimization. Finally, since generating bytecode is faster than generating Full-codegen’s baseline compiled code, activating Ignition generally improves script startup times and in turn, web page loads.

    ​ ——《 Launching Ignition and TurboFan

  • 之前的编译器无法支持和优化ES新的语法特性

    ​ The TurboFan project originally started in late 2013 to address the shortcomings of Crankshaft. Crankshaft can only optimize a subset of the JavaScript language. For example, it was not designed to optimize JavaScript code using structured exception handling, i.e. code blocks demarcated by JavaScript’s try, catch, and finally keywords. It is difficult to add support for new language features in Crankshaft, since these features almost always require writing architecture-specific code for nine supported platforms. Furthermore, Crankshaft’s architecture is limited in the extent that it can generate optimal machine code. It can only squeeze so much performance out of JavaScript, despite requiring the V8 team to maintain more than ten thousand lines of code per chip architecture.

    ​ ——《 Launching Ignition and TurboFan

2.3 - V8(5.9版)之后——Ignition解释器和TurboFan编译器

​ 和5.9版之前直接生成基准机器码的方式不同,5.9版之后(包括5.9)生成了基准bytecode,这样既降低了基准码的大小,又利于很多关于bytecode的优化策略实现,新的执行流程如下:

  1. 和之前基本一样,parser将JS代码解析成AST;
  2. 独立的线程通过Ignition解释器将AST解释成bytecode,并清除AST以释放内存空间——使用Ignition解释器直接解释执行bytecode——这里的bytecode同时作为基准执行模型(bytecode的大小只有等效机器码的25%-50%);
  3. 在代码运行过程中,Ignition解释器会收集用于优化的信息并传给TurboFan编译器TurboFan编译器会根据这些信息和基准bytecode生成经过优化的机器码,并执行;(所以新架构总体来说是一种bytecode执行和优化机器码并行的架构)
  4. 同样的,这期间内存管理的线程和GC的线程也在配合编译执行。
几点优化方案与deoptimization:
优化方案:
  1. 函数只是声明而未调用,不会解析生成AST;
  2. 函数只被调用一次,会在bytecode层直接被解释执行;
  3. 函数被调用多次,可能会被标记成热点函数,可能会被编译成机器码。
deoptimization:

​ 一些情况下,优化机器码也可能被逆向还原成基准bytecode,这个过程被称为deoptimization

​ 如上述优化方案中,一个热点函数被编译成机器码,由于内联缓存,机器码预设了传入参数的数据类型。若之后的执行过程中,传入的参数类型有所改变,V8引擎则需要将这段机器码逆向还原成bytecode解释执行。

2.4 - 基于编译方案的JavaScript优化书写方式

  • 总是以相同的顺序实例化对象的属性,这样隐藏类及之后的优化代码都可以被共享;
  • 尽量在对象构造函数中赋值对象的所有属性,实例化后为对象添加属性会致使之前隐藏类优化方法变慢;
  • 不随意更改函数参数的数据类型,减少deoptimization;
  • 避免使用键不是递增数字的稀疏数组。稀疏数组中没有包含每个元素的数组称为一个哈希表。访问该数组中的元素会更加耗时。同样地,试着避免预先分配大型数组。最好是随着你使用而递增。最后,不要删除数组中的元素。这会让键稀疏。
  • V8 用 32 位来表示对象和数字。它使用一位来辨别是对象(flag=1)或者是被称为 SMI(小整数) 的整数(flag=0),之所以是小整数是因为它是 31 位的。之后,如果一个数值比 31 位还要大,V8 将会装箱数字,把它转化为浮点数并且创建一个新的对象来存储这个数字。尽可能试着使用 31 位有符号数字来避免创建 JS 对象的耗时装箱操作。

——本博客是旦旦看了B站up主objtube《【干货】8分钟带你了解V8引擎是如何运行JS!都2020年了还不知道什么是V8?》后,结合《how JavaScript work》整理的一份笔记,如有错漏,欢迎指出。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值