V8引擎详解(五)——内联缓存

前言

本文是V8引擎详解系列的第五篇,重点内容是关于V8引擎的内联缓存,V8之因此能够高效的运行,其内部实现了不少优化策略,其中 内联缓存 就是其中很重要的一个优化策略,本文会从一个小问题开始一块儿探究到底什么是 内联缓存(Inline Cache) ,简称 IC。文末会有已经完成的系列文章的连接,本系列文章还在不断更新欢迎持续关注。缓存

先抛一个问题

咱们先用一个看一个小例子bash

let length = 10000;
let obj0 = {x: 1, y: 2, z: 3};
function func(o) { 
    for(let i in o) {
        o[i].toString();
    }
}
复制代码
console.time('t0');   // 计时开始
for (let i = 0; i < length; i++) {
    let obj1 = {x: 3, y: 2}; // 为了保证建立对象消耗时间保持一致
    obj1[i] = 3;
    func(obj0);
}
console.timeEnd('t0'); // 计时结束
复制代码

咱们先看一下结果
t0: 8.047119140625ms函数

而后咱们再看下一段代码 惟一的区别只是func调用的是obj1 以下:post

console.time('t1');   // 计时开始
for (let i = 0; i < length; i++) {
    let obj1 = {x: 3, y: 2};
    obj1[i] = 3;
    func(obj1);
}
console.timeEnd('t1'); // 计时结束
复制代码

咱们再来看一下结果
t1: 14.747314453125ms性能

咱们能够看到消耗的时间差别,而咱们今天要说的内容就是产生这种差别的主要缘由 内联缓存的机制学习

内联缓存

什么是内联缓存

首先内联缓存(后面称IC)也并非V8独创,这项技术也很古老了,最初是应用在Smalltalk虚拟机上。IC的原理简单来讲就是在运行过程当中,收集一些数据信息,将这部分信息缓存起来而后在再次执行的时候能够直接利用这些信息,有效的节省了再次获取这些信息的消耗,从而提升性能。优化

举个例子:好比咱们的使用一个对象obj = {x: 1, y: 2}的时候,若是咱们调用了obj.x 咱们会将obj.x缓存起来,当咱们再次调用obj.x的时候直接使用缓存好的信息就能够了,而不用再从新获取obj.x的值。ui

内联缓存是怎么运做的

咱们能够经过分析一段字节码的运行来看一下内联缓存的运做,若是不了解字节码执行的同窗能够先看V8引擎详解(四)——字节码是如何执行的 先来了解一下。
先来看下面一段代码spa

function test(obj) {
 obj.y = 4;
 obj.x += 2;
 return obj.x;
}
test({x: 1, y: 2});
复制代码

将function转成字节码的结构如图:3d

咱们分析一下这段字节码(本文重点在于IC因此会侧重于涉及IC部分):

  • 进入函数先进行栈的检查,而后会将小数字4存入累加器。

  • 将累加器的值传给 a0[0] (obj.y), 同时将 **a0[0] (obj.y)**的信息缓存到 反馈向量 表中的第0个 slot插槽)中。

  • 加载 a0[1] (obj.x) 的值到累加器中同时将 **a0[1] (obj.x)**的信息缓存到 反馈向量 表中的第2个 slot插槽)中。

  • 将累加器中的值加2,将结果值缓存到反馈向量 表中的第4个 slot插槽)中,而后将累加器中的值赋予到a0[1] (obj.x) ,并将信息缓存到反馈向量 表中的第5个 slot插槽)中。

  • 最后当咱们将obj.x中的值直接经过缓存取出到累加器中并将累加器中的值返回。

运行过程并不复杂,本质上就是标记一些调用点,而后为他们分配一个插槽缓存起来,当再次调用的时候直接经过缓存获取值。

内联缓存的单态与多态

事实上咱们在调用函数的时候,能够经过缓存信息提升函数的执行效率,可是前提是咱们传参的结构是固定的,那若是传递参数的结构不是固定的内联缓存要如何处理呢?

这个就涉及到咱们开篇的那个问题了,回到代码来看:

console.time('t0');   // 计时开始
for (let i = 0; i < length; i++) {
    let obj1 = {x: 3, y: 2}; // 为了保证建立对象消耗时间保持一致
    obj1[i] = 3;
    func(obj0);
}
console.timeEnd('t0'); // 计时结束
复制代码

这段代码中func调用的是固定的结构 obj0 = {x: 1, y: 2, y: 3},因此能够经过内联缓存加速在执行上效率大大提升。
可是第二段代码中:

console.time('t1');   // 计时开始
for (let i = 0; i < length; i++) {
    let obj1 = {x: 3, y: 2};
    obj1[i] = 3;
    func(obj1);
}
console.timeEnd('t1'); // 计时结束
复制代码

func调用的obj2 每次执行结构都是变化的(i的值一直在变),那么v8是如何处理的

这里面就涉及到了多态内联缓存Polymorphic Inline Cache)也就是PIC,所谓的PIC就是在同一个 Slot 位置上,不只只缓存一份数据如图:

(图片来源:李兵老师的专栏 图解V8引擎

第一次执行函数的时候,v8会将对象的一些信息记录到slot中,第二次执行函数会将第一次记录的信息和第二次的信息进行比较,若是相同直接调用,若是不一样会将这部分信息记录在同一个位置。依次类推一个slot会记录多份信息(固然也是有必定数量限制的)。 同一个slot记录多个信息的状况就能够称之为PIC多态内联缓存,而多态内联缓存可能会进行屡次的比较操做,天然性能上不如单态内联缓存,也就是为何开篇中第二段函数执行的比第一段来的慢。

总结

本文主要学习了V8引擎中的内联缓存,同时也解释了在分析字节码的时候常常会看到最后面那个额外的值(反馈向量)的做用。实际上,在咱们实际的开发过程当中内联缓存的多态状况是不可避免的,V8针对这种状况也作了大量的优化,其实绝大部分状况是彻底感觉不到差别的,咱们写代码的时候了解就能够了,不必针对这个特地进行优化。
若是有什么错误,请在评论中和做者一块儿讨论,若是您以为本文对您有帮助请帮忙点个赞,感激涕零。

参考文章

time.geekbang.org/column/arti…

系列文章

V8引擎详解(一)——概述
V8引擎详解(二)——AST
V8引擎详解(三)——从字节码看V8的演变
V8引擎详解(四)——字节码是如何执行的
V8引擎详解(五)——内联缓存
V8引擎详解(六)——内存结构

相关文章
相关标签/搜索