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.047119140625msmarkdown

而後咱們再看下一段代碼 惟一的區別只是func調用的是obj1 以下:函數

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.747314453125msoop

咱們能夠看到消耗的時間差別,而咱們今天要說的內容就是產生這種差別的主要緣由 內聯緩存的機制post

內聯緩存

什麼是內聯緩存

首先內聯緩存(後面稱IC)也並非V8獨創,這項技術也很古老了,最初是應用在Smalltalk虛擬機上。IC的原理簡單來講就是在運行過程當中,收集一些數據信息,將這部分信息緩存起來而後在再次執行的時候能夠直接利用這些信息,有效的節省了再次獲取這些信息的消耗,從而提升性能。性能

舉個例子:好比咱們的使用一個對象obj = {x: 1, y: 2}的時候,若是咱們調用了obj.x 咱們會將obj.x緩存起來,當咱們再次調用obj.x的時候直接使用緩存好的信息就能夠了,而不用再從新獲取obj.x的值。學習

內聯緩存是怎麼運做的

咱們能夠經過分析一段字節碼的運行來看一下內聯緩存的運做,若是不瞭解字節碼執行的同窗能夠先看V8引擎詳解(四)——字節碼是如何執行的 先來了解一下。
先來看下面一段代碼優化

function test(obj) {
 obj.y = 4;
 obj.x += 2;
 return obj.x;
}
test({x: 1, y: 2});
複製代碼

將function轉成字節碼的結構如圖:spa

咱們分析一下這段字節碼(本文重點在於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引擎詳解(六)——內存結構
V8引擎詳解(七)——垃圾回收機制
V8引擎詳解(八)——消息隊列
V8引擎詳解(九)——協程&生成器函數

相關文章
相關標籤/搜索