V8执行原理和优化策略
浏览器内核
- Gecko: 早起被 Netscape 和 Mozila firefox 浏览器使用。
- Trident: 微软开发,被 IE4~IE1 浏览器使用,但是 Edge 浏览器使用 Blink.
- Webkit: 评估给予 KHTML 开发、开源的,用来Safari、Google Chrome 之前也是使用的。
- Blink: 是 Webkit 的一个分支,Google 开发,应用于 Google Chrome、Edge 、Opera。
浏览器内核主要是负责浏览器的排版引擎,也称为浏览器引擎、页面渲染引擎或样板引擎。
Js 引擎
- js 代码是交付给 js 引擎来执行的。 我们编写的无论是浏览器或者node的 js,最后都需要被 CPU 执行。
- 但是 CPU 只识别自己的指令,机器语言,才能被 CPU 执行。
- 我们通过 js 引擎 来帮助我们把代码翻译成 CPU 指令来执行。
- js 引擎有:
- SpiderMonkey: 第一款 js 引擎。
- Chakra: 微软开发,IE 浏览器。
- JavaScriptCore: Webkit 中的 js 引擎, Apple 开发。
- V8: Google 开发。
V8 引擎
- V8 是用 C++ 编写的 Google 开源高性能的 javaScript 引擎。
- 用于 Chrome 和 NodeJs 中。
- V8 可以独立运行,也可以潜入到任何 C++ 应用程序。
编译器和解析器
机器不能够直接理解我们所写的高级语言代码,在执行之前,需要将代翻译成机器能够读懂的机器语言。
-
编译型语言,需要经过编译器的编译过程,编译之后会保留下来二进制文件,每次运行程序,直接运行该二进制文件,不必要再次重新编译。 c/c++, go.
-
解释型语言编写的程序,在每次运行时都需要通过解释器对程序进行动态解释和执行。 js/python
在解析型语言的解释过程中,解释器会对源代码进行词法分析、语法分析,然后生成抽象语法树(AST),基于抽象语法树生成字节码,最后再根据字节码来执行程序、输出结果。
V8 执行原理
V8在执行 javaScript 过程中既有解释器 Ignition, 也有编译器 TurboFan.
- 生成抽象语法树 (AST) 和执行上下文
无论是使用的是解释器还是编译器都是无法理解高级语言的。它们可以理解的就是 AST。
var a = 42;
var b = 5;
function addA(d) {
return a + d;
}
var c = addA(2) + b;
AST 类似于代码结构化的表示,编译器和解释器的后续工作都要依靠 AST。
- 生成 AST 的阶段。
- 第一个阶段分词(词法分析)。将一行行源码拆分为一个个 token。token就是语法上不能够再分的、最小的单个字符或字符串。
- 第二个阶段解析(语法分析)。将上一步生成的 token 数据,根据语法规则转为 AST。如果源码符合规则,就会顺利转位 AST。如果错误,就会存在语法错误,然后终止,抛出一个语法错误。
-
有了 AST 之后,V8旧会生成该段代码的执行上下文。
-
生成字节码。解释器 Ignition 会根据 AST 生成字节码,并解释执行字节码。
一开始的 V8 是会直接将 AST 转位机器码。但是随着 Chrome 在手机上的普及,特别是运行在
512M 内存的手机上,内存占用问题就会暴露明显。V8需要消耗大量的内存来存放转换后的机器吗。
V8 进行了大幅度的重构了引擎架构,引入字节码,并且抛弃了之前的编译器。实现了现在的架构。
字节码:就是介于 AST 和机器码之间的一种代码。字节码需要通过解释器将其转为机器码后才能执行。
- 执行代码
如果有一段第一次执行的字节码,解释器 Ignition 会逐条解释执行。在 Ignition
的过程中,如果发现有热点代码,比如一段代码被重复执行多次,这种就是热点代码。
后台的编译器 TurboFan 会把该段热点的字节码编译位高效的机器码,然后当再次执行
这段被优化的代码时,只需要执行编译后的机器码旧可以了。
热点代码都被编译器 TurboFan 转换了机器吗,直接执行机器吗旧省去了字节码翻译为机器吗的过程。
字节码配合解释器和编译器的技术被称为即使编译 JIT.
- 注意:机器码实际上也会被还原为字节码.
如果经过 TurboFan 编译过后的优化机器码,在后续执行过程中,发现发生了改变,之前优化的机器码并不能正确的处理运算,就会逆向的转换成字节码。(反优化)。
测试
console.time('time');
function foo(a, b) {
return a + b;
}
var a = 1;
var b = 2;
for (var i = 0; i < 1000000; i++) {
foo(a , b);
}
for (var i = 0; i < 1000000; i++) {
foo(a , b);
}
console.timeEnd('time');
最终耗时 time: 69.06396484375 ms
console.time('time');
function foo(a, b) {
return a + b;
}
var a = 1;
var b = 2;
for (var i = 0; i < 10000000; i++) {
foo(a , b);
}
foo(a, '1');
for (var i = 0; i < 10000000; i++) {
foo(a , b);
}
console.timeEnd('time');
最终耗时 time: 102.06396484375 ms
从上代码我们可以,就是因为 TurboFan 已经给函数字节码标记为热点代码,但是之后类型转换,迫使优化后的机器码失效,又重新进行了转回字节点的反向操作,对性能造成很大的破坏。
上述 V8 在执行时候。 首先将上述的源码经过Parse模块解析转为AST,然后Ignition将AST转换为字节码,在字节码解析执行AST的时候,给函数标记为热点代码,转换为优化后的机器码,之后 foo(a, ‘1’) 破坏了原先优化机器码,迫使转回到字节码,之后 for 循环又再次标记热点代码,最后输出机器码。这就是 V8引擎做的事情,之后机器码就是 CPU 来执行了。
V8 优化策略
随着V8团队对解释器和编译器的不断优化,一些小的优化策略已经不是那么的关注了。
- 脚本流
- 正常流程应该是 下载 -> 解析 -> 执行
- 优化策略,下载的过程中如果超过 30kb,就会新开一个线程去解析,而不是等待下载全部结束在去解析。
- 字节码缓存
- JIT 技术
- 懒解析。
- 先不去解析函数内部逻辑,用到再去解析
函数优化
- lazy parsing 懒解析: 不需要执行就不去解析, 这是懒解析. 但是如果我们声明函数,马上就会执行,这时候反过头去再去解析函数,势必就会带来性能问题。
- eager parsing 饥饿解析: 遇到函数就去解析函数,等待下面真正执行的时候,就不需要回头去解析这个函数。
- 使用,需要给需要立即解析的函数加上一对括号。 var add = ((a, b) => a + b); 完事了。
对象优化
- 相同顺序初始化对象成员
虽然说 js 是动态类型的,变量是没有类型的(值才有),但是在编译器解析的时候,会给对象的属性赋予一个类型(叫做 hiddenClass, 多达21种类型),以相同顺序初始化对象成员,可以避免类的调整。
const person1 = {name: 'leo'}; // hiddenClass-1
person1.age = 18; // hiddenClass-2
const person2 = {age: 18}; // hiddenClass-3
person2.name = 'lisa'; // hiddenClass-4
这样做的并不会对 hiddenClass 进行复用,即使是一样的,但是在 V8 内部上的实现是会将每个属性都放入一个有顺序的容器(数组之类的),所以不是相同顺序对象成员,会进行类的调整,造成性能上的问题。
const person1 = {name: 'leo'}; // hiddenClass-1
person1.age = 18; // hiddenClass-2
const person2 = {name: 'lisa'}; // hiddenClass-1
person2.age = 20; // hiddenClass-2
这样子就就可以进行复用了。不会心创建 hiddenClass.
- 实例化之后避免添加新属性
const person1 = {name: 'leo'}; // In-object 属性,一开始就存在上面的属性。
person1.age = 18; // Normal/Fast 属性, 是存储在 property store 需要间接查找属性。
- 避免使用类数组
数组会进行极大的一个优化, 但是类数组却不会。
Array.prototype.forEach(arrayLike, (value, index) => {});
V8 推荐,先把 类数组转为数组, 这样的效率要比call调用数组的方法要快。
var arr = Array.prototype.slice.call(arrayLike, 0);
arr.forEach(() => {});
-
避免读取超过数组的长度
存在数组越界问题,arr[arr.length] 这种查找不到属性,就会沿着原型链找一遍,最后返回一个 undefined. -
避免元素类型转换
const array = [3, 2, 1]; // PACKED_SMI_ELEMENTS
PACKED: 是代表元素的是满的,元素并不存在 null、undefined 这样的.
SMI: 代表是 Int 类型
array.push(4.4); // PACKED_DOUBLE_ELEMENTS
PACKED: 是代表元素的是满的,元素并不存在 null、undefined 这样的.
DOUBLE: 代表是 DOUBLE 类型.
是一个降级的过程:越降级越通用,但是优化越少。
HOLEY_DOUBLE_ELEMENTS; 也是向下的降级,代表有 null、undefined 的元素。
主要关注优化点
对于优化 JavaScript 执行效率,应该将优化的中心聚焦在单次脚本执行的时间和脚本的网络下载上。
- 提升单次脚本的执行速度,避免 js 的长任务霸占主现场,使的页面快速响应交互。
- 避免大的内联脚本,在解析 HTML 的过程中, 解析和编译会占用主线程。
- 减少 js 文件的容量,可以提升网络下载速度,占用更低的内存。