天天看點

簡明扼要地談談v8的隐藏類和Inline Cache(內聯緩存)

還有一個是從AST直接生成機器碼,但是這個現在已經被換成了Ignition(解釋器)+TurboFan(類型優化編譯器)的架構了。

先說隐藏類:對一個JS對象的屬性通路而言,最簡單的解釋器實作會把屬性模組化為運作時的hash<string, object>查詢。然而這個性能太慢,怎麼優化呢?簡單的說就是參考靜态編譯器的思路,把屬性field的按名字通路,抹掉名字資訊,變成按offset通路。——不過這樣就需要一個class的描述符資料結構。

The implementation of an IC is called a stub. Stubs behave like functions in the sense that you call them, and they return, but they don't necessarily set up a stack frame and follow the full calling convention. Stubs are usually generated on the fly, but for common cases, they can be cached and reused by multiple ICs. The stub which implements an IC typically contains optimized code which handles the types of operands that particular IC has encountered in the past (which is why it's called a cache). If the stub encounters a case it's not prepared to handle, it "misses" and calls the C++ runtime code. The runtime handles the case, then generates a new stub which can handle the missed case (as well as previous cases). The call to the old stub in the full compiled code is rewritten to call the new stub, and execution resumes as if the stub had been called normally.

JS是一門動态類型的語言,但要讓JS運作時變快,就要盡量在運作時作為靜态類型的語言來處理。——當然,這個方面做到極緻就是要考慮CPU片内緩存的編譯器後端優化。。。

OK,接下來說Inline Cache。IC其實就是把運作時動态的類型switch-case直接特化生成單獨的Stub函數版本。比如,舉個例子來說,我們要實作一個圖像的矩形區域copyRect算法。這個圖像的像素格式可以是RGBA,也可能是A8灰階類型。一般人可能實作一個copy就是在一個循環裡if-else處理。但是這會導緻CPU片内緩存頻繁失效,導緻很差的性能。是以正确的做法就是為不同的像素格式實作2個版本的copyRect函數,然後根據輸入圖像的像素類型直接跳轉/調用不同的copyRect函數。

像以前的GDI+、或者Skia這些2D圖形引擎,其raster子產品的BitBlit,都是這麼做的。

舉JS裡面的代碼執行個體來說,譬如屬性通路:a.x。假如沒有IC的話,那麼标準實作隻能是一個解釋器:——需要判斷x這個屬性到底哪裡來的,是“Own”屬性呢?還是基類Prototype上的,亦或者說是不存在的。這麼做(運作時if-else / switch-case)很浪費性能,是以用IC來實作,就是判斷a對象的class類型,(通過class描述符),這就能夠知道屬性x究竟是什麼類型,然後生成對應的Stub函數(此Stub函數就是運作時特定于,比如說,x是Own屬性的情況),直接調用執行。

當然,由于是把動态語言的if-else解釋器執行的本質,通過IC,換成了“預先判斷出對象的當時當地的類型 + 生成特定于這種類型的Stub函數 + 執行特化的JIT Stub函數”,等于是切換到了靜态執行的性質。當然前提是運作時必須作檢查,以保證此類型是沒有問題的。

于是,IC在我看來,就是一種适合于JIT編譯器的優化思想,而不是某種程式設計技巧。——當然,說到技巧,也就是那個特定于不同CPU ABI的OSR了,或者稱不同Stack Frame之間的跳轉(解釋器棧 <--> JIT生成的Native棧)。

繼續閱讀