js性能優化之monomorphism

以前看了wizard大神關於js極致優化的演講,優化主要包括3個方向html

  1. monomorphism
  2. bit operator
  3. bloom filter

下來本身也搜了一些關於monomorphism的文章,這裏作一個總結。java

monomorphism是啥

直譯過來是單一形態,什麼意思呢? 大概就是說咱雖然是動態類型的語言吧,可是最好也別太飄了,對象的類型能穩定就穩定,別總是作刪屬性,增屬性這種操做。引用v8大神Vyacheslav Egorov的話來講就是:緩存

nicest dynamic behavior is static-likebash

怎麼翻譯我也不知道,總之就是代碼寫的越像靜態的越好(感受在變相給ts點贊)。閉包

對象是怎存的

要弄清楚緣由,咱得先弄明白咱得對象都是怎麼存的。app

在一些靜態類型語言裏面,好比java,由於類的每一個屬性的類型已經定了,因此每一個類型要佔的內存大小也能夠肯定,因此在compile的時候,每一個屬性的offset等信息都是能夠肯定的。這樣一來,訪問一個屬性就會很是快。函數

而在js這種動態類型的語言裏面,compile的時候顯然無法知道每一個屬性在內存裏的offset究竟是多少。 那js裏面是怎麼找到屬性的內存地址的呢?性能

hidden class

js的vm,好比v8,會給每個咱們建立的對象,都生成一個對應的hidden class。 這個hidden class上會存屬性的一些metadata以及在內存裏的offset。測試

那麼問題來了?這些值直接存object上不就行了麼?幹嗎還再搞一個hidden class的概念出來?優化

緣由是爲了節省內存,hidden class是能夠共享的。 好比下面這例子:

let a = {x: 2, y: 3}
    let b = {x: 3, y: 4}
複製代碼

對象a和b的property以及類型徹底一致,咱們徹底可使用一個hidden class來描述這兩個對象。 如此一來,管你成千上萬個對象,只要你的屬性同樣,一個hidden class就搞定了。

若是對象b動態的增長了一個屬z,會是什麼狀況?

顯然,咱們無法直接取修改以前的hidden class, 由於對象a並無也增長這麼一個屬性. 取而代之的操做是會生成一個新的hidden class,描述新的屬性c的metadata和offset。 而且新生成的hidden class會指向以前的hidden class,這個結構的學名叫Transition Chain。當咱們訪問一個對象屬性的時候,會先從最新的hidden class找起。

若是b這個對象繼續增長新的屬性,transition chain上的hidden class也會愈來愈多。那麼就會有一個問題,加如我新加了100個屬性,那爲了找到最開始的屬性值,我得遍歷100個hidden class才能拿到最終的offset。 這個性能顯然很差。

inline cache

爲了解決這個問題,v8採用了inline cache

inline cache背後的概念很簡單:當我對對象和屬性類型的預測是對的,那中間的計算就省略了,直接從緩存裏取offset值就完事了。

ok,inline cache很牛逼,但它都在哪些地方用呢?

js裏的每一個函數都會被封裝在一個閉包裏面,閉包裏面除了函數以外, 就是一堆對hidden class的inline cache。

好比這個例子:

function getX(o) {
    return o.x;
}

getX({x: 1})
getX({x: 2})
複製代碼

函數getX在第一遍被調用後,會用{x:1}的hidden class做爲cache entry作好緩存。第二遍調用的時候,由於hidden class同樣,因此就直接從緩存裏拿offset了。

須要注意的一點是,inline cache的cache entry並非只有一個,能夠有多個,cache entry的數量直接決定了讀取的性能。 根據cache entry數量不一樣,inline cache分爲了3種狀態:

  1. monomorphic: 就一個cache entry,這樣速率是最快的。
  2. polymorphic:1 < cache entry <= 4。 這個狀態下,會在entry裏挨個匹配,匹配的速率就取決於我們機器跑conditional branch的速率了。
  3. megamorphic: cache entry > 4。 這個狀態就麻煩了,由於hidden class的type太多了,因此會被存到一個global的哈希表裏面。在有衝突的狀況下,直接覆蓋。(這裏其實我理解了好久,以前一直以爲哈希表讀取的時間也是常量,是很是快的。但果真仍是太年輕了啊,哈希表把key映射到內存的哈希函數實際上是很是耗性能的,這裏有一篇v8優化這個過程的文章)。 固然了,megamorphic的性能仍是會比去跑transition chain好上很多的。

那三種狀態究竟速度差多少呢? 能夠看下這個測試:

megamorphic狀態下的對象讀操做大概要比monomorphic慢86%左右,性能差異能夠說是很是顯著了。

參考文獻

A sneak peek into super optimized code in JS frameworks by Maxim Koretskyi | JSConf EU 2019

What's up with monomorphism? Explaining JavaScript VMs in JavaScript - Inline Caches

Optimizing hash tables: hiding the hash code

相關文章
相關標籤/搜索