摘要:摘要性能彪悍的引擎。深入淺出系列深入淺出第課箭頭函數(shù)中的究竟是什么鬼深入淺出第課函數(shù)是一等公民是什么意思呢深入淺出第課什么是垃圾回收算法深入淺出第課是如何工作的最近,生態(tài)系統(tǒng)又多了個(gè)非常硬核的項(xiàng)目。
摘要: 性能彪悍的V8引擎。
《JavaScript深入淺出》系列:
JavaScript深入淺出第1課:箭頭函數(shù)中的this究竟是什么鬼?
JavaScript深入淺出第2課:函數(shù)是一等公民是什么意思呢?
JavaScript深入淺出第3課:什么是垃圾回收算法?
JavaScript深入淺出第4課:V8是如何工作的?
最近,JavaScript生態(tài)系統(tǒng)又多了2個(gè)非常硬核的項(xiàng)目。
大神Fabrice Bellard發(fā)布了一個(gè)新的JS引擎QuickJS,可以將JavaScript源碼轉(zhuǎn)換為C語言代碼,然后再使用系統(tǒng)編譯器(gcc或者clang)生成可執(zhí)行文件。
Facebook為React Native開發(fā)了新的JS引擎Hermes,用于優(yōu)化安卓端的性能。它可以在構(gòu)建APP的時(shí)候?qū)avaScript源碼編譯為Bytecode,從而減少APK大小、減少內(nèi)存使用,提高APP啟動(dòng)速度。
作為JavaScript程序員,只有極少數(shù)人有機(jī)會(huì)和能力去實(shí)現(xiàn)一個(gè)JS引擎,但是理解JS引擎還是很有必要的。本文將介紹一下V8引擎的原理,希望可以給大家一些幫助。
JavaScript引擎我們寫的JavaScript代碼直接交給瀏覽器或者Node執(zhí)行時(shí),底層的CPU是不認(rèn)識(shí)的,也沒法執(zhí)行。CPU只認(rèn)識(shí)自己的指令集,指令集對(duì)應(yīng)的是匯編代碼。寫匯編代碼是一件很痛苦的事情,比如,我們要計(jì)算N階乘的話,只需要7行的遞歸函數(shù):
function factorial(N) { if (N === 1) { return 1; } else { return N * factorial(N - 1); } }
代碼邏輯也非常清晰,與階乘的數(shù)學(xué)定義完美吻合,哪怕不會(huì)寫代碼的人也能看懂。
但是,如果使用匯編語言來寫N階乘的話,要300+行代碼n-factorial.s:
這個(gè)N階乘的匯編代碼是我大學(xué)時(shí)期寫的,已經(jīng)是N年前的事情了,它需要處理10進(jìn)制與2進(jìn)制的轉(zhuǎn)換,需要使用多個(gè)字節(jié)保存大整數(shù),最多可以計(jì)算大概500左右的N階乘。
還有一點(diǎn),不同類型的CPU的指令集是不一樣的,那就意味著得給每一種CPU重寫匯編代碼,這就很崩潰了。。。
還好,JavaScirpt引擎可以將JS代碼編譯為不同CPU(Intel, ARM以及MIPS等)對(duì)應(yīng)的匯編代碼,這樣我們才不要去翻閱每個(gè)CPU的指令集手冊(cè)。當(dāng)然,JavaScript引擎的工作也不只是編譯代碼,它還要負(fù)責(zé)執(zhí)行代碼、分配內(nèi)存以及垃圾回收。
雖然瀏覽器非常多,但是主流的JavaScirpt引擎其實(shí)很少,畢竟開發(fā)一個(gè)JavaScript引擎是一件非常復(fù)雜的事情。比較出名的JS引擎有這些:
V8 (Google)
SpiderMonkey (Mozilla)
JavaScriptCore (Apple)
Chakra (Microsoft)
IOT:duktape、JerryScript
還有,最近發(fā)布QuickJS與Hermes也是JS引擎,它們都超越了瀏覽器范疇,Atwood"s Law再次得到了證明:
Any application that can be written in JavaScript, will eventually be written in JavaScript.V8:強(qiáng)大的JavaScript引擎
在為數(shù)不多JavaScript引擎中,V8無疑是最流行的,Chrome與Node.js都使用了V8引擎,Chrome的市場占有率高達(dá)60%,而Node.js是JS后端編程的事實(shí)標(biāo)準(zhǔn)。國內(nèi)的眾多瀏覽器,其實(shí)都是基于Chromium瀏覽器開發(fā),而Chromium相當(dāng)于開源版本的Chrome,自然也是基于V8引擎的。神奇的是,就連瀏覽器界的獨(dú)樹一幟的Microsoft也投靠了Chromium陣營。另外,Electron是基于Node.js與Chromium開發(fā)桌面應(yīng)用,也是基于V8的。
V8引擎是2008年發(fā)布的,它的命名靈感來自超級(jí)性能車的V8引擎,敢于這樣命名確實(shí)需要一些實(shí)力,它性能確實(shí)一直在穩(wěn)步提高,下面是使用Speedometer benchmark的測(cè)試結(jié)果:
圖片來源:https://v8.dev/
V8在工業(yè)界已經(jīng)非常成功了,同時(shí)它還獲得了學(xué)術(shù)界的肯定,拿到了ACM SIGPLAN的Programming Languages Software Award:
V8"s success is in large part due to the efficient machine code it generates.
Because JavaScript is a highly dynamic object-oriented language, many experts believed that this level of performance could not be achieved.
V8"s performance breakthrough has had a major impact on the adoption of JavaScript, which is nowadays used on the browser, the server, and probably tomorrow on the small devices of the internet-of-things.
JavaScript是一門動(dòng)態(tài)類型語言,這會(huì)給編譯器增加很大難度,因此專家們覺得它的性能很難提高,但是V8居然做到了,生成了非常高效的machine code(其實(shí)是匯編代碼),這使得JS可以應(yīng)用在各個(gè)領(lǐng)域,比如Web、APP、桌面端、服務(wù)端以及IOT。
嚴(yán)格來講,V8所生成的代碼是匯編代碼而非機(jī)器代碼,但是V8相關(guān)的文檔、博客以及其他資料都把V8生成的代碼稱作machine code。匯編代碼與機(jī)器代碼很多是一一對(duì)應(yīng)的,也很容易互相轉(zhuǎn)換,這也是反編譯的原理,因此他們把V8生成的代碼稱為Machine Code也未嘗不可,但是并不嚴(yán)謹(jǐn)。
V8引擎的內(nèi)部結(jié)構(gòu)V8是一個(gè)非常復(fù)雜的項(xiàng)目,使用cloc統(tǒng)計(jì)可知,它竟然有超過100萬行C++代碼。
V8由許多子模塊構(gòu)成,其中這4個(gè)模塊是最重要的:
Parser:負(fù)責(zé)將JavaScript源碼轉(zhuǎn)換為Abstract Syntax Tree (AST)
Ignition:interpreter,即解釋器,負(fù)責(zé)將AST轉(zhuǎn)換為Bytecode,解釋執(zhí)行Bytecode;同時(shí)收集TurboFan優(yōu)化編譯所需的信息,比如函數(shù)參數(shù)的類型;
TurboFan:compiler,即編譯器,利用Ignitio所收集的類型信息,將Bytecode轉(zhuǎn)換為優(yōu)化的匯編代碼;
Orinoco:garbage collector,垃圾回收模塊,負(fù)責(zé)將程序不再需要的內(nèi)存空間回收;
其中,Parser,Ignition以及TurboFan可以將JS源碼編譯為匯編代碼,其流程圖如下:
簡單地說,Parser將JS源碼轉(zhuǎn)換為AST,然后Ignition將AST轉(zhuǎn)換為Bytecode,最后TurboFan將Bytecode轉(zhuǎn)換為經(jīng)過優(yōu)化的Machine Code(實(shí)際上是匯編代碼)。
如果函數(shù)沒有被調(diào)用,則V8不會(huì)去編譯它。
如果函數(shù)只被調(diào)用1次,則Ignition將其編譯Bytecode就直接解釋執(zhí)行了。TurboFan不會(huì)進(jìn)行優(yōu)化編譯,因?yàn)樗枰狪gnition收集函數(shù)執(zhí)行時(shí)的類型信息。這就要求函數(shù)至少需要執(zhí)行1次,TurboFan才有可能進(jìn)行優(yōu)化編譯。
如果函數(shù)被調(diào)用多次,則它有可能會(huì)被識(shí)別為熱點(diǎn)函數(shù),且Ignition收集的類型信息證明可以進(jìn)行優(yōu)化編譯的話,這時(shí)TurboFan則會(huì)將Bytecode編譯為Optimized Machine Code,以提高代碼的執(zhí)行性能。
圖片中的紅線是逆向的,這的確有點(diǎn)奇怪,Optimized Machine Code會(huì)被還原為Bytecode,這個(gè)過程叫做Deoptimization。這是因?yàn)镮gnition收集的信息可能是錯(cuò)誤的,比如add函數(shù)的參數(shù)之前是整數(shù),后來又變成了字符串。生成的Optimized Machine Code已經(jīng)假定add函數(shù)的參數(shù)是整數(shù),那當(dāng)然是錯(cuò)誤的,于是需要進(jìn)行Deoptimization。
function add(x, y) { return x + y; } add(1, 2); add("1", "2");
在運(yùn)行C、C++以及Java等程序之前,需要進(jìn)行編譯,不能直接執(zhí)行源碼;但對(duì)于JavaScript來說,我們可以直接執(zhí)行源碼(比如:node server.js),它是在運(yùn)行的時(shí)候先編譯再執(zhí)行,這種方式被稱為即時(shí)編譯(Just-in-time compilation),簡稱為JIT。因此,V8也屬于JIT編譯器。
Ignition:解釋器Node.js是基于V8引擎實(shí)現(xiàn)的,因此node命令提供了很多V8引擎的選項(xiàng),使用node的--print-bytecode選項(xiàng),可以打印出Ignition生成的Bytecode。
factorial.js如下,由于V8不會(huì)編譯沒有被調(diào)用的函數(shù),因此需要在最后一行調(diào)用factorial函數(shù)。
function factorial(N) { if (N === 1) { return 1; } else { return N * factorial(N - 1); } } factorial(10); // V8不會(huì)編譯沒有被調(diào)用的函數(shù),因此這一行不能省略
使用node命令(node版本為12.6.0)的--print-bytecode選項(xiàng),打印出Ignition生成的Bytecode:
node --print-bytecode factorial.js
控制臺(tái)輸出的內(nèi)容非常多,最后一部分是factorial函數(shù)的Bytecode:
[generated bytecode for function: factorial] Parameter count 2 Register count 3 Frame size 24 18 E> 0x3541c2da112e @ 0 : a5 StackCheck 28 S> 0x3541c2da112f @ 1 : 0c 01 LdaSmi [1] 34 E> 0x3541c2da1131 @ 3 : 68 02 00 TestEqualStrict a0, [0] 0x3541c2da1134 @ 6 : 99 05 JumpIfFalse [5] (0x3541c2da1139 @ 11) 51 S> 0x3541c2da1136 @ 8 : 0c 01 LdaSmi [1] 60 S> 0x3541c2da1138 @ 10 : a9 Return 82 S> 0x3541c2da1139 @ 11 : 1b 04 LdaImmutableCurrentContextSlot [4] 0x3541c2da113b @ 13 : 26 fa Star r1 0x3541c2da113d @ 15 : 25 02 Ldar a0 105 E> 0x3541c2da113f @ 17 : 41 01 02 SubSmi [1], [2] 0x3541c2da1142 @ 20 : 26 f9 Star r2 93 E> 0x3541c2da1144 @ 22 : 5d fa f9 03 CallUndefinedReceiver1 r1, r2, [3] 91 E> 0x3541c2da1148 @ 26 : 36 02 01 Mul a0, [1] 110 S> 0x3541c2da114b @ 29 : a9 Return Constant pool (size = 0) Handler Table (size = 0)
生成的Bytecode其實(shí)挺簡單的:
使用LdaSmi命令將整數(shù)1保存到寄存器;
使用TestEqualStrict命令比較參數(shù)a0與1的大??;
如果a0與1相等,則JumpIfFalse命令不會(huì)跳轉(zhuǎn),繼續(xù)執(zhí)行下一行代碼;
如果a0與1不相等,則JumpIfFalse命令會(huì)跳轉(zhuǎn)到內(nèi)存地址0x3541c2da1139
...
不難發(fā)現(xiàn),Bytecode某種程度上就是匯編語言,只是它沒有對(duì)應(yīng)特定的CPU,或者說它對(duì)應(yīng)的是虛擬的CPU。這樣的話,生成Bytecode時(shí)簡單很多,無需為不同的CPU生產(chǎn)不同的代碼。要知道,V8支持9種不同的CPU,引入一個(gè)中間層Bytecode,可以簡化V8的編譯流程,提高可擴(kuò)展性。
如果我們?cè)诓煌布先ド葿ytecode,會(huì)發(fā)現(xiàn)生成代碼的指令是一樣的:
圖片來源:Ross McIlroy
使用node命令的--print-code以及--print-opt-code選項(xiàng),打印出TurboFan生成的匯編代碼:
node --print-code --print-opt-code factorial.js
我是在Mac上運(yùn)行的,結(jié)果如下圖所示:
比起B(yǎng)ytecode,正真的匯編代碼可讀性差很多。而且,機(jī)器的CPU類型不一樣的話,生成的匯編代碼也不一樣。
這些匯編代碼就不用去管它了,因?yàn)樽钪匾氖抢斫釺urboFan是如何優(yōu)化所生成的匯編代碼的。我們可以通過add函數(shù)來梳理整個(gè)優(yōu)化過程。
function add(x, y) { return x + y; } add(1, 2); add(3, 4); add(5, 6); add("7", "8");
由于JS的變量是沒有類型的,所以add函數(shù)的參數(shù)可以是任意類型:Number、String、Boolean等,這就意味著add函數(shù)可能是數(shù)字相加(V8還會(huì)區(qū)分整數(shù)和浮點(diǎn)數(shù)),可能是字符串拼接,也可能是其他更復(fù)雜的操作。如果直接編譯的話,生成的代碼比如會(huì)有很多if...else分支,偽代碼如下:
if (isInteger(x) && isInteger(y)) { // 整數(shù)相加 } else if (isFloat(x) && isFloat(y)) { // 浮點(diǎn)數(shù)相加 } else if (isString(x) && isString(y)) { // 字符串拼接 } else { // 各種其他情況 }
我只寫了4個(gè)分支,實(shí)際上的分支其實(shí)更多,比如當(dāng)參數(shù)類型不一致時(shí)還得進(jìn)行類型轉(zhuǎn)換,大家不妨看看ECMASCript對(duì)加法是如何定義的:12.8.3The Addition Operator ( + )。
如果直接按照偽代碼去生成匯編代碼,那生成的代碼必然非常冗長,這樣會(huì)占用很多內(nèi)存空間。
Ignition在執(zhí)行add(1, 2)時(shí),已經(jīng)知道add函數(shù)的兩個(gè)參數(shù)都是整數(shù),那么TurboFan在編譯Bytecode時(shí),就可以假定add函數(shù)的參數(shù)是整數(shù),這樣可以極大地簡化生成的匯編代碼,偽代碼如下:
if (isInteger(x) && isInteger(y)) { // 整數(shù)相加 } else { // Deoptimization }
當(dāng)然這樣做也是有風(fēng)險(xiǎn)的,因?yàn)槿绻鸻dd函數(shù)參數(shù)不是整數(shù),那么生成的匯編代碼也沒法執(zhí)行,只能Deoptimize為Bytecode來執(zhí)行。
也就是說,如果TurboFan對(duì)add函數(shù)進(jìn)行編譯優(yōu)化的話,則add(3, 4)與add(3, 4)可以執(zhí)行優(yōu)化的匯編代碼,但是add("7", "8")只能Deoptimize為Bytecode來執(zhí)行。
當(dāng)然,TurboFan所做的也不只是根據(jù)類型信息來簡化代碼執(zhí)行流程,它還會(huì)進(jìn)行其他優(yōu)化,比如減少冗余代碼等更復(fù)雜的事情。
由這個(gè)簡單的例子可知,如果我們的JS代碼中變量的類型變來變?nèi)?,是?huì)給V8引擎增加不少麻煩的,為了提高性能,我們可以盡量不要去改變變量的類型。
對(duì)于性能要求比較高的項(xiàng)目,使用TypeScript也是不錯(cuò)的選擇,理論上,如果嚴(yán)格遵守類型化的編程方式,也是可以提高性能的,類型化的代碼有利于V8引擎優(yōu)化編譯的匯編代碼,當(dāng)然這一點(diǎn)還需要測(cè)試數(shù)據(jù)來證明。
Orinoco:垃圾回收強(qiáng)大的垃圾回收功能是V8實(shí)現(xiàn)提高性能的關(guān)鍵之一,因?yàn)樗梢栽诒苊庥绊慗S代碼執(zhí)行的情況下,同時(shí)回收內(nèi)存空間,提高內(nèi)存利用效率。
關(guān)于垃圾回收,我在JavaScript深入淺出第3課:什么是垃圾回收算法?中有詳細(xì)介紹,這里就不再贅述了。
JS引擎的未來V8引擎確實(shí)很強(qiáng)大,但是它也不是無所不能的,簡單地分析都可以發(fā)現(xiàn)一些可以優(yōu)化的點(diǎn)。
我有一個(gè)新的想法,還沒想好名字,不妨稱作Optimized TypeScript Engine:
使用TypeScript編程,遵循嚴(yán)格的類型化編程規(guī)則,不要寫成AnyScript了;
構(gòu)建的時(shí)候?qū)ypeScript直接編譯為Bytecode,而不是生成JS文件,這樣運(yùn)行的時(shí)候就省去了Parse以及生成Bytecode的過程;
運(yùn)行的時(shí)候,需要先將Bytecode編譯為對(duì)應(yīng)CPU的匯編代碼;
由于采用了類型化的編程方式,有利于編譯器優(yōu)化所生成的匯編代碼,省去了很多額外的操作;
這個(gè)想法其實(shí)可以基于V8引擎來實(shí)現(xiàn),技術(shù)上應(yīng)該是可行的:
將Parser以及Ignition拆分出來,用于構(gòu)建階段;
刪掉TurboFan處理JS動(dòng)態(tài)特性的相關(guān)代碼;
這樣做,可以將JS引擎簡化很多,一方面不再需要parse以及生成bytecode,另一方面編譯器不再需要因?yàn)镴avaScript動(dòng)態(tài)特性做很多額外的工作。因此可以減少CPU、內(nèi)存以及電量的使用,優(yōu)化性能,唯一的問題可能是必須使用嚴(yán)格的TS語法進(jìn)行編程。
為啥要這樣做呢?因?yàn)閷?duì)于IOT硬件來說,CPU、內(nèi)存、電量都是需要省著點(diǎn)用的,不是每一個(gè)智能家電都需要裝一個(gè)驍龍855,如果希望把JS應(yīng)用到IOT領(lǐng)域,必然需要從JS引擎角度去進(jìn)行優(yōu)化,只是去做上層的框架是沒有用的。
其實(shí),F(xiàn)acebook的Hermes差不多就是這么干的,只是它沒有要求用TS編程。
這應(yīng)該是JS引擎的未來,大家會(huì)看到越來越多這樣的趨勢(shì)。
關(guān)于JS,我打算花1年時(shí)間寫一個(gè)系列的博客《JavaScript深入淺出》,大家還有啥不太清楚的地方?不妨留言一下,我可以研究一下,然后再與大家分享一下。歡迎添加我的個(gè)人微信(KiwenLau),我是Fundebug的技術(shù)負(fù)責(zé)人,一個(gè)對(duì)JS又愛又恨的程序員。
參考Celebrating 10 years of V8
Launching Ignition and TurboFan
JavaScript engines - how do they even?
An Introduction to Speculative Optimization in V8
Understanding V8’s Bytecode
2018年,JavaScript都經(jīng)歷了什么?
JavaScript深入淺出第3課:什么是垃圾回收算法?
Fabrice Bellard 是個(gè)什么水平的程序員?
如何評(píng)價(jià) Fabrice Bellard 發(fā)布 QuickJS JS 引擎?
關(guān)于FundebugFundebug專注于JavaScript、微信小程序、微信小游戲、支付寶小程序、React Native、Node.js和Java線上應(yīng)用實(shí)時(shí)BUG監(jiān)控。 自從2016年雙十一正式上線,F(xiàn)undebug累計(jì)處理了10億+錯(cuò)誤事件,付費(fèi)客戶有陽光保險(xiǎn)、核桃編程、荔枝FM、掌門1對(duì)1、微脈、青團(tuán)社等眾多品牌企業(yè)。歡迎大家免費(fèi)試用!
版權(quán)聲明轉(zhuǎn)載時(shí)請(qǐng)注明作者 Fundebug以及本文地址:
https://blog.fundebug.com/2019/07/16/how-does-v8-work/
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://m.hztianpu.com/yun/110252.html
摘要:所做的最重要的事情,就是對(duì)成千上萬的網(wǎng)頁進(jìn)行排序,所以它存在的意義是基于網(wǎng)頁的。確實(shí)有很多非常成功的產(chǎn)品,比如,,,但是它們其實(shí)都是收購來的。為什么呢因?yàn)橐龅綐O簡主義,需要深刻思考用戶需求以及產(chǎn)品價(jià)值。 摘要: Chrome改變世界。 《JavaScript深入淺出》系列: JavaScript深入淺出第1課:箭頭函數(shù)中的this究竟是什么鬼? JavaScript深入淺出第2課:...
摘要:摘要是如何回收內(nèi)存的深入淺出系列深入淺出第課箭頭函數(shù)中的究竟是什么鬼深入淺出第課函數(shù)是一等公民是什么意思呢深入淺出第課什么是垃圾回收算法最近垃圾回收這個(gè)話題非?;?,大家不能隨隨便便的扔垃圾了,還得先分類,這樣方便對(duì)垃圾進(jìn)行回收再利用。 摘要: JS是如何回收內(nèi)存的? 《JavaScript深入淺出》系列: JavaScript深入淺出第1課:箭頭函數(shù)中的this究竟是什么鬼? Jav...
摘要:引擎可以是一個(gè)標(biāo)準(zhǔn)的解釋器,也可以是一個(gè)將編譯成某種形式的字節(jié)碼的即時(shí)編譯器。和其他引擎最主要的差別在于,不會(huì)生成任何字節(jié)碼或是中間代碼。不使用中間字節(jié)碼的表示方式,就沒有必要用解釋器了。 原文地址:https://blog.sessionstack.com... showImg(https://segmentfault.com/img/bVVwZ8?w=395&h=395); 數(shù)周之...
摘要:第二篇文章將深入谷歌的引擎的內(nèi)部。引擎可以實(shí)現(xiàn)為標(biāo)準(zhǔn)解釋器,或者以某種形式將編譯為字節(jié)碼的即時(shí)編譯器。這個(gè)引擎是在谷歌中使用的,但是,與其他引擎不同的是也用于流行的。一種更復(fù)雜的優(yōu)化編譯器,生成高度優(yōu)化的代碼。不是唯一能夠做到的引擎。 本系列的 第一篇文章 主要介紹引擎、運(yùn)行時(shí)和調(diào)用堆棧。第二篇文章將深入谷歌 V8 的JavaScript引擎的內(nèi)部。 想閱讀更多優(yōu)質(zhì)文章請(qǐng)猛戳GitHu...
摘要:第二篇文章將深入谷歌的引擎的內(nèi)部。引擎可以實(shí)現(xiàn)為標(biāo)準(zhǔn)解釋器,或者以某種形式將編譯為字節(jié)碼的即時(shí)編譯器。這個(gè)引擎是在谷歌中使用的,但是,與其他引擎不同的是也用于流行的。一種更復(fù)雜的優(yōu)化編譯器,生成高度優(yōu)化的代碼。不是唯一能夠做到的引擎。 本系列的 第一篇文章 主要介紹引擎、運(yùn)行時(shí)和調(diào)用堆棧。第二篇文章將深入谷歌 V8 的JavaScript引擎的內(nèi)部。 想閱讀更多優(yōu)質(zhì)文章請(qǐng)猛戳GitHu...
閱讀 3951·2021-07-28 18:10
閱讀 2643·2019-08-30 15:44
閱讀 1162·2019-08-30 14:07
閱讀 3516·2019-08-29 17:20
閱讀 1643·2019-08-26 18:35
閱讀 3597·2019-08-26 13:42
閱讀 1871·2019-08-26 11:58
閱讀 1664·2019-08-23 18:33