【第942期】圖說 WebAssembly
前言
昨天在朋友圈圍觀了首屆VueConf大會。今日早讀文章由@明非翻譯授權分享。
正文從這開始~
最近,WebAssembly 在 JavaScript 圈非常的火!人們都在談論它多麼多麼快,怎樣怎樣改變 Web 開發領域。但是沒有人講他到底為什麼那麼快。在這篇文章里,我將會幫你了解 WebAssembly 到底為什麼那麼快。
第一,我們需要知道它到底是什麼!WebAssembly 是一種可以使用非 JavaScript 編程語言編寫代碼並且能在瀏覽器上運行的技術方案。
當大家談論起 WebAssembly 時,首先想到的就是 JavaScript。現在,我沒有必須在 WebAssembly 和 JavaScript 中選一個的意思。實際上,我們期待開發者在一個項目中把 WebAssembly 和 JavaScript 結合使用。但是,比較這兩者是有用的,這對你了解 WebAssembly 有一定幫助。
拓展閱讀閱讀:
一點點性能歷史
1995 年 JavaScript 誕生。它的設計時間非常短,前十年發展迅速。
緊接著瀏覽器廠商們就開始了更多的競爭。
2008年,人們稱之為瀏覽器性能大戰的時期開始了。很多瀏覽器加入了即時編譯器,又稱之為JITs。在這種模式下,JavaScript在運行的時候,JIT 選擇模式然後基於這些模式使代碼運行更快。
這些 JITs 的引入是瀏覽器運行代碼機制的一個轉折點。所有的突然之間,JavaScript 的運行速度快了 10 倍。
隨著這種改進的性能,JavaScript 開始被用於意想不到的事情,比如使用Node.js和Electron構建應用程序。
現在 WebAssembly 可能是的另一個轉折點。
在我們沒有搞清楚 JavaScript 和 WebAssembly 之間的性能差前,我們需要理解 JS 引擎所做的工作。
JavaScript 是如何在瀏覽器中運行的呢?
作為一個開發人員,您將JavaScript添加到頁面時,您有一個目標並遇到一個問題。
目標:你想要告訴計算機做什麼
問題:你和計算機使用不通的語言。
您說的是人類的語言,計算機說的是機器語言。儘管你不認為 JavaScript 或者其他高級語言是人類語言,但事實就是這樣的。它們的設計是為了讓人們認知,不是為機器設計的。
所以JavaScript引擎的工作就是把你的人類語言轉化成機器所理解的語言。
我想到電影《Arrival》,這就像人類和外星人進行交談。
在這部電影中,人類語言不能從逐字翻譯成外星語言。他們的語言反映出兩種對世界不同的認知。人類和機器也是這樣。
所以,怎麼進行翻譯呢?
在編程中,通常有兩種翻譯方法將代碼翻譯成機器語言。你可以使用解釋器或者編譯器。
使用解釋器,翻譯的過程基本上是一行一行及時生效的。
編譯器是另外一種工作方式,它在執行前翻譯。
每種翻譯方法都有利弊。
解釋器的利弊
解釋器很快的獲取代碼並且執行。您不需要在您可以執行代碼的時候知道全部的編譯步驟。因此,解釋器感覺與 JavaScript 有著自然的契合。web 開發者能夠立即得到反饋很重要。
這也是瀏覽器最開始使用 JavaScript 解釋器的原因之一。
但是實用解釋器的弊端是當nin運行相同的代碼的時候。比如,您執行了一個循環。然後您就會一遍又一遍的做同樣的事情。
編譯器的利弊
編譯器則有相反的效果。在程序開始的時候,它可能需要稍微多一點的時間來了解整個編譯的步驟。但是當運行一個循環的時候他會更快,因為他不需要重複的去翻譯每一次循環里的代碼。
因為解釋器必須在每次循環訪問時不斷重新轉換代碼,作為一個可以擺脫解釋器低效率的方法,瀏覽器開始將編譯器引入。
不同的瀏覽器實現起來稍有不同,但是基本目的是相同的。他們給 JavaScript 引擎添加了一個新的部分,稱為監視器(也稱為分析器)。該監視器在 JavaScript 運行時監控代碼,並記錄代碼片段運行的次數以及使用了那些數據類型。
如果相同的代碼行運行了幾次,這段代碼被標記為 「warm」。如果運行次數比較多,就被標記為 「hot」。
被標記為 「warm」 的代碼被扔給基礎編譯器,只能提升一點點的速度。被標記為 「hot」 的代碼被扔給優化編譯器,速度提升的更多。
耗時比較:JavaScript Vs. WebAssembly
這張圖大致給出了現在一個程序的啟動性能,目前 JIT 編譯器在瀏覽器中很常見。
該圖顯示了 JS 引擎運行程序花費的時間。顯示的時間並不是平均的。這個圖片表明,JS 引擎做的這些任務花費的時間取決於頁面中 JavaScript 做了什麼事情。但是我們可以用這個圖來構建一個心理模型。
每欄顯示花費在特定任務上的時間。
Parsing - 講源碼轉換成解釋器可以運行的東西所用的事情。
Compiling + optimizing - 花費在基礎編譯和優化編譯上的時間。有一些優化編譯的工作不在主線程,所以這裡並不包括這些時間。
Re-optimizing - 當預先編譯優化的代碼不能被優化的情況下,JIT 將這些代碼重新優化,如果不能重新優化那麼久丟給基礎編譯去做。這個過程叫做重新優化。
Execution - 執行代碼的過程
Garbage collection - 清理內存的時間
一個重要的事情要注意:這些任務不會發生在離散塊或特定的序列中。相反,它們將被交叉執行。比如正在做一些代碼解析時,還執行者一些其他的邏輯,有些代碼編譯完成後,引擎又做了一些解析,然後又執行了一些邏輯,等等。
這種交叉執行對早期 JavaScript 的性能有很大的幫助,早期的 JavaScript 的執行就像下圖一樣:
一開始,當只有一個解釋器運行 JavaScript 時,執行速度相當緩慢。JITs 的引入,大大提升了執行效率。
監視和編譯代碼的開銷是需要權衡的事情。如果 JavaScript 開發人員按照相同的方式編寫JavaScript,解析和編譯時間將會很小。但是,性能的提升使開發人員能夠創建更大的JavaScript應用程序。
這意味著還有改進的餘地。
下面是 WebAssembly 如何比較典型 web 應用。
瀏覽器的 JS 引擎有輕微的不同。我是基於 SpiderMonkey 來講。
請求
這沒有展示在圖上,但是從伺服器獲取文件是會消耗時間的
下載執行與 JavaScript 等效的 WebAssembly 文件需要更少的時間,因為它的體積更小。WebAssembly 設計的體積更小,可以以二進位形式表示。
即使使用 gzip 壓縮的 JavaScript文件很小,但 WebAssembly 中的等效代碼可能更小。
所以說,下載資源的時間會更少。在網速慢的情況下更能顯示出效果來。
解析
JavaScript 源碼一旦被下載到瀏覽器,源將被解析為抽象語法樹(AST)。
通常瀏覽器解析源碼是懶惰的,瀏覽器首先會解析他們真正需要的東西,沒有及時被調用的函數只會被創建成存根。
在這個過程中,AST被轉換為該 JS 引擎的中間表示(稱為位元組碼)。
相反,WebAssembly 不需要被轉換,因為它已經是位元組碼了。它僅僅需要被解碼並確定沒有任何錯誤。
編譯 + 優化
如前所述,JavaScript 是在執行代碼期間編譯的。因為 JavaScript 是動態類型語言,相同的代碼在多次執行中都有可能都因為代碼里含有不同的類型數據被重新編譯。這樣會消耗時間。
相反,WebAssembly 與機器代碼更接近。例如,類型是程序的一部分。這是速度更快的一個原因:
編譯器不需要在運行代碼時花費時間去觀察代碼中的數據類型,在開始編譯時做優化。
編譯器不需要去每次執行相同代碼中數據類型是否一樣。
更多的優化在 LLVM 最前面就已經完成了。所以編譯和優化的工作很少。
重新優化
有時 JIT 拋出一個優化版本的代碼,然後重新優化。
JIT 基於運行代碼的假設不正確時,會發生這種情況。例如,當進入循環的變數與先前的迭代不同時,或者在原型鏈中插入新函數時,會發生重新優化。
在 WebAssembly 中,類型是明確的,因此 JIT 不需要根據運行時收集的數據對類型進行假設。這意味著它不必經過重新優化的周期。
執行
儘可能編寫執行性能好的 JavaScript。所以,你可能需要知道 JIT 是如何做優化的。
然而,大多數開發者並不知道 JIT 的內部原理。即使是那些了解 JIT 內部原理的開發人員,也很難實現最佳的方案。有很多時候,人們為了使他們的代碼更易於閱讀(例如:將常見任務抽象為跨類型工作的函數)會阻礙編譯器優化代碼。
正因如此,執行 WebAssembly 代碼通常更快。有些必須對 JavaScript 做的優化不需要用在 WebAssembly 上
另外,WebAssembly 是為編譯器設計的。意思是,它是專門給編譯器來閱讀,並不是當做編程語言讓程序員去寫的。
由於程序員不需要直接編程,WebAssembly 提供了一組更適合機器的指令。根據您的代碼所做的工作,這些指令的運行速度可以在10%到800%之間。
垃圾回收
在 JavaScript 中,開發者不需要擔心內存中無用變數的回收。JS 引擎使用一個叫垃圾回收器的東西來自動進行垃圾回收處理。
這對於控制性能可能並不是一件好事。你並不能控制垃圾回收時機,所以它可能在非常重要的時間去工作,從而影響性能。
現在,WebAssembly 根本不支持垃圾回收。內存是手動管理的(就像 C/C++)。雖然這些可能讓開發者編程更困難,但它的確提升了性能。
總而言之,這些都是在許多情況下,在執行相同任務時WebAssembly 將勝過 JavaScript 的原因。
在某些情況下,WebAssembly 不能像預期的那樣執行,還有一些更改使其更快。我在另一篇文章中更深入地介紹了這些未來的功能。
WebAssembly 是如何工作的?
現在,您了解開發人員為什麼對 WebAssembly 感到興奮,讓我們來看看它是如何工作的。
當我談到上面的 JIT 時,我談到了與機器的溝通像與外星人溝通。
我現在想看看這個外星人的大腦如何工作 - 機器的大腦如何解析和理解交流內容。
這個大腦的一部分是專註于思考,例如算術和邏輯。有一部分腦部提供短期記憶,另一部分提供長期記憶。
這些不同的部分都有名字。
負責思考的部分是算術邏輯單元(ALU)。
短期儲存由寄存器(Registers)提供。
隨機存儲器(或RAM)來提供長期儲存能力。
機器碼中的語句被稱為指令。
當一條指令進入大腦時會發生什麼?它被拆分成了多個的部分並有特殊的含義。
被拆分成的多個部分分別進入不同的大腦單元進行處理,這也是拆分指令所依賴的方式。
例如,這個大腦從機器碼中取出4-10位,並將它們發送到 ALU。ALU進行計算,它根據 0 和 1 的位置來確定是否需要將兩個數相加。
這個塊被稱為「操作碼」,因為它告訴 ALU 執行什麼操作。
那麼這個大腦會拿後面的兩個塊來確定他們所要操作的數。這兩個塊對應的是寄存器的地址。
請注意添加在機器碼上面的標註(ADD R1 R2),這使我們更容易了解發生了什麼。這就是彙編。它被稱為符號機器碼。這樣人類也能看懂機器碼的含義。
您可以看到,這個機器的彙編和機器碼之間有非常直接的關係。每種機器內部有不同的結構,所以每種機器都有自己獨有的彙編語言。
所以我們並不只有一個翻譯的目標。
相反,我們的目標是不同類型的機器碼。就像人類說不同的語言一樣,機器也有不同的語言。
您希望能夠將這些任何一種高級編程語言轉換為任何一種彙編語言。這樣做的一個方法是創建一大堆不同的翻譯器,可以從任意一種語言轉換成任意一種彙編語言。
這樣做的效率非常低。為了解決這個問題,大多數編譯器會在高級語言和彙編語言之間多加一層。編譯器將把高級語言翻譯成一種更低級的語言,但比機器碼的等級高。這就是中間代碼(IR)。
意思就是編譯器可以將任何一種高級語言轉換成一種中間語言。然後,編譯器的另外的部分將中間語言編譯成目標機器的彙編代碼。
編譯器的「前端」將高級編程語言轉換為IR。編譯器的「後端」將 IR 轉換成目標機器的彙編代碼。
WebAssembly 適合在哪裡使用?
您可能會將 WebAssembly 當做是另外一種目標彙編語言。這是真的,這些機器語言(x86,ARM等)中的每一種都對應於特定的機器架構。
當你的代碼運行在用戶的機器的 web 平台上的時候,你不知道你的代碼將會運行在那種機器結構上。
所以 WebAssembly 和別的彙編語言是有一些不同的。所以他是一個概念機上的機器語言,不是在一個真正存在的物理機上運行的機器語言。
正因如此,WebAssembly 指令有時候被稱為虛擬指令。它比 JavaScript 代碼更快更直接的轉換成機器代碼,但它們不直接和特定硬體的特定機器代碼對應。
在瀏覽器下載 WebAssembly後,使 WebAssembly 的迅速轉換成目標機器的彙編代碼。
如果想在您的頁面里上添加 WebAssembly,您需要將您的代碼編譯成 .wasm 文件。
編譯到 .wasm 文件
當前對 WebAssembly 支持最多的編譯器工具鏈稱是 LLVM。有許多不同的「前端」和「後端」可以插入到 LLVM 中。
注意:大多數 WebAssembly 模塊開發者使用 C 和 Rust 編寫代碼,然後編譯成 WebAssembly,但是這裡有其他創建 WebAssembly 模塊的途徑。比如,這裡有一個實驗性工具,他可以幫你使用 TypeScript 創建一個 WebAssembly 模塊,你可以在這裡直接編輯WebAssembly。
架設我們想通過 C 來創建 WebAssembly。我們可以使用 clang 「前端」 從 C 編譯成 LLVM 中間代碼。當它變成 LLVM 的中間代碼(IR)以後,LLVM 可以理解他,所以 LLVM 可以對代碼做一些優化。
如果想讓 LLVM 的 IR 變成 WebAssembly,我們需要一個 「後端」。目前 LLVM 項目中有一個正在開發中的。這個「後端」對做這件事情很重要,應該很快就會完成。可惜,它現在還不能用。
另外有一個工具叫做 Emscripten,它用起來比較簡單。它還可以有比較有用的可以選擇,比如說由 IndexDB 支持的文件系統。
不管你使用的什麼工具鏈,最終的結果都應該是以 .wasm 結尾的文件。來讓我們看一下如何將它用在你的 web 頁面。
在 JavaScript 中載入一個 .wasm 組件
.wasm 文件是 WebAssembly 組件,它可以被 JavaScript 載入。到目前為止,載入過程有點複雜。
functionfetchAndInstantiate(url,importObject){
returnfetch(url).then(response=>
response.arrayBuffer()
).then(bytes=>
WebAssembly.instantiate(bytes,importObject)
).then(results=>
results.instance
);
}
您可以在文檔中更深入地了解這些。
我們正在努力使這個過程更容易。我們期望對工具鏈進行改進,並與現有的模塊管理工具(如Webpack)或載入器(如SystemJS)相結合。我相信,載入 WebAssembly 模塊越來越簡單,就像載入 JavaScript 一樣。
但是,WebAssembly模塊和JS模塊之間存在重大差異。目前,WebAssembly 中的函數只能使用 WebAssembly 類型(整數或浮點數)作為參數或返回值。
對於任何更複雜的數據類型(如字元串),必須使用 WebAssembly 模塊的內存。
如果你之前主要使用 JavaScript,可能對於直接訪問內存是不熟悉的。C,C ++和Rust等性能更高的語言往往具有手動內存管理功能。WebAssembly 模塊的內存模擬這些語言中的堆。
為此,它使用 JavaScript 中稱為 ArrayBuffer。ArrayBuffer 是一個位元組數組。因此,數組的索引作為內存地址。
如果要在 JavaScript 和 WebAssembly 之間傳遞一個字元串,需要將字元轉換為等效的字元碼。然後你需要將它寫入內存數組。由於索引是整數,所以可以將索引傳遞給 WebAssembly 函數。因此,字元串的第一個字元的索引可以當作指針。
任何人開發的 WebAssembly 模塊很可能被 Web 開發人員使用並為該模塊創建一個的裝飾器。這樣,您當做用戶來使用這個模塊就不需要考慮內存管理的事情了。
※讀《前端架構設計》-前端架構概況
※Square 公司怎麼寫提交信息
※深入淺出React和Redux
※關於前端團隊架構的思考
※你不懂JS:ES6與未來 集合
TAG:前端早讀課 |
※夏季新款Nike Air Vapormax Flyknit 2.0 大氣墊跑步鞋 942842-006
※燈 Marcel Rieder (1862 –1942)
※蕙柏推出博士Bose QuietControl 30無線耳機電池ABI400942
※分析WordPress遠程執行代碼漏洞CVE-2019-8942和CVE-2019-8943
※廣東原廠虎撲正品NIKE AIR VAPORMAX FLYKNIT 2 男子跑步鞋 942842-012
※Steam平台上架一款詐騙遊戲 內容全照搬《戰地1942》
※史蒂芬·霍金 1942-2018R.I.P
※教育商務演示 松下 PT-BW400C僅9942元
※2.0 W 二代大氣墊百搭慢跑鞋「2.0白冰藍」942842-100
※德國裝甲作戰的探路者——第三裝甲集團軍(1938.10——1942.1)
※工商銀行2019H1營收3942.03億元,同比增長9.1%
※《海賊王》漫畫第942話公布「SMILE」果實充滿罪惡
※從精英到二流——二戰德國第三裝甲集團軍(1942.1——1945.5)
※史蒂文霍金1942-2017
※1941-1942:陳納德和他的美國志願者
※流產的毛蔣會晤:1942—1943年國共關係再考察(五)
※流產的毛蔣會晤:1942—1943年國共關係再考察(二)
※補充資料:二戰德國國防軍第2集團軍1941.9—1942.1戰鬥序列
※1942、1954,1966,1978,1990,2002出生屬馬人2018運勢!
※納粹德國二十二個集團軍群之B集團軍群(1942.8——1943.2)