V8执行原理和优化策略

V8执行原理和优化策略

浏览器内核

  1. Gecko: 早起被 Netscape 和 Mozila firefox 浏览器使用。
  2. Trident: 微软开发,被 IE4~IE1 浏览器使用,但是 Edge 浏览器使用 Blink.
  3. Webkit: 评估给予 KHTML 开发、开源的,用来Safari、Google Chrome 之前也是使用的。
  4. Blink: 是 Webkit 的一个分支,Google 开发,应用于 Google Chrome、Edge 、Opera。
    浏览器内核主要是负责浏览器的排版引擎,也称为浏览器引擎、页面渲染引擎或样板引擎。

Js 引擎

  1. js 代码是交付给 js 引擎来执行的。 我们编写的无论是浏览器或者node的 js,最后都需要被 CPU 执行。
  2. 但是 CPU 只识别自己的指令,机器语言,才能被 CPU 执行。
  3. 我们通过 js 引擎 来帮助我们把代码翻译成 CPU 指令来执行。
  4. js 引擎有:
    • SpiderMonkey: 第一款 js 引擎。
    • Chakra: 微软开发,IE 浏览器。
    • JavaScriptCore: Webkit 中的 js 引擎, Apple 开发。
    • V8: Google 开发。

V8 引擎

  1. V8 是用 C++ 编写的 Google 开源高性能的 javaScript 引擎。
  2. 用于 Chrome 和 NodeJs 中。
  3. V8 可以独立运行,也可以潜入到任何 C++ 应用程序。

编译器和解析器

机器不能够直接理解我们所写的高级语言代码,在执行之前,需要将代翻译成机器能够读懂的机器语言。

  1. 编译型语言,需要经过编译器的编译过程,编译之后会保留下来二进制文件,每次运行程序,直接运行该二进制文件,不必要再次重新编译。 c/c++, go.

  2. 解释型语言编写的程序,在每次运行时都需要通过解释器对程序进行动态解释和执行。 js/python

在解析型语言的解释过程中,解释器会对源代码进行词法分析、语法分析,然后生成抽象语法树(AST),基于抽象语法树生成字节码,最后再根据字节码来执行程序、输出结果。

V8 执行原理

V8在执行 javaScript 过程中既有解释器 Ignition, 也有编译器 TurboFan.

  1. 生成抽象语法树 (AST) 和执行上下文
    无论是使用的是解释器还是编译器都是无法理解高级语言的。它们可以理解的就是 AST。
var a = 42;
var b = 5;
function addA(d) {
    return a + d;
}
var c = addA(2) + b;

AST 类似于代码结构化的表示,编译器和解释器的后续工作都要依靠 AST。

  1. 生成 AST 的阶段。
  • 第一个阶段分词(词法分析)。将一行行源码拆分为一个个 token。token就是语法上不能够再分的、最小的单个字符或字符串。
  • 第二个阶段解析(语法分析)。将上一步生成的 token 数据,根据语法规则转为 AST。如果源码符合规则,就会顺利转位 AST。如果错误,就会存在语法错误,然后终止,抛出一个语法错误。
  1. 有了 AST 之后,V8旧会生成该段代码的执行上下文。

  2. 生成字节码。解释器 Ignition 会根据 AST 生成字节码,并解释执行字节码。
    一开始的 V8 是会直接将 AST 转位机器码。但是随着 Chrome 在手机上的普及,特别是运行在
    512M 内存的手机上,内存占用问题就会暴露明显。V8需要消耗大量的内存来存放转换后的机器吗。
    V8 进行了大幅度的重构了引擎架构,引入字节码,并且抛弃了之前的编译器。实现了现在的架构。

字节码:就是介于 AST 和机器码之间的一种代码。字节码需要通过解释器将其转为机器码后才能执行。

  1. 执行代码
    如果有一段第一次执行的字节码,解释器 Ignition 会逐条解释执行。在 Ignition
    的过程中,如果发现有热点代码,比如一段代码被重复执行多次,这种就是热点代码。
    后台的编译器 TurboFan 会把该段热点的字节码编译位高效的机器码,然后当再次执行
    这段被优化的代码时,只需要执行编译后的机器码旧可以了。

热点代码都被编译器 TurboFan 转换了机器吗,直接执行机器吗旧省去了字节码翻译为机器吗的过程。

字节码配合解释器和编译器的技术被称为即使编译 JIT.

  1. 注意:机器码实际上也会被还原为字节码.
    如果经过 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团队对解释器和编译器的不断优化,一些小的优化策略已经不是那么的关注了。

  1. 脚本流
    • 正常流程应该是 下载 -> 解析 -> 执行
    • 优化策略,下载的过程中如果超过 30kb,就会新开一个线程去解析,而不是等待下载全部结束在去解析。
  2. 字节码缓存
    • JIT 技术
  3. 懒解析。
    • 先不去解析函数内部逻辑,用到再去解析

函数优化

  1. lazy parsing 懒解析: 不需要执行就不去解析, 这是懒解析. 但是如果我们声明函数,马上就会执行,这时候反过头去再去解析函数,势必就会带来性能问题。
  2. eager parsing 饥饿解析: 遇到函数就去解析函数,等待下面真正执行的时候,就不需要回头去解析这个函数。
  3. 使用,需要给需要立即解析的函数加上一对括号。 var add = ((a, b) => a + b); 完事了。

对象优化

  1. 相同顺序初始化对象成员
    虽然说 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.

  1. 实例化之后避免添加新属性
const person1 = {name: 'leo'}; // In-object 属性,一开始就存在上面的属性。

person1.age = 18; // Normal/Fast 属性, 是存储在 property store 需要间接查找属性。
  1. 避免使用类数组
    数组会进行极大的一个优化, 但是类数组却不会。
Array.prototype.forEach(arrayLike, (value, index) => {});

V8 推荐,先把 类数组转为数组, 这样的效率要比call调用数组的方法要快。

var arr = Array.prototype.slice.call(arrayLike, 0);
arr.forEach(() => {});
  1. 避免读取超过数组的长度
    存在数组越界问题,arr[arr.length] 这种查找不到属性,就会沿着原型链找一遍,最后返回一个 undefined.

  2. 避免元素类型转换

const array = [3, 2, 1];  // PACKED_SMI_ELEMENTS
PACKED: 是代表元素的是满的,元素并不存在 nullundefined 这样的.
SMI: 代表是 Int 类型

array.push(4.4);    // PACKED_DOUBLE_ELEMENTS 
PACKED: 是代表元素的是满的,元素并不存在 nullundefined 这样的.
DOUBLE: 代表是 DOUBLE 类型.

是一个降级的过程:越降级越通用,但是优化越少。
HOLEY_DOUBLE_ELEMENTS;  也是向下的降级,代表有 nullundefined 的元素。

主要关注优化点

对于优化 JavaScript 执行效率,应该将优化的中心聚焦在单次脚本执行的时间和脚本的网络下载上。

  1. 提升单次脚本的执行速度,避免 js 的长任务霸占主现场,使的页面快速响应交互。
  2. 避免大的内联脚本,在解析 HTML 的过程中, 解析和编译会占用主线程。
  3. 减少 js 文件的容量,可以提升网络下载速度,占用更低的内存。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值