WebAssembly學習(一):認識WebAssembly


WebAssembly作為一門新興起的技術,在 JavaScript 圈非常的火!人們都在談論它多么多么快,怎樣怎樣改變 Web 開發領域,被各大巨頭所推廣,這篇文章對其做一個簡單的了解認識,本文非原創,參考文章見底部。

1.什么是WebAssembly

WebAssembly的名字帶個匯編Assembly,所以我們從其名字上就能知道其意思是給Web使用的匯編語言,是通過Web執行低級二進制語法。

但是WebAssembly並不是直接用匯編語言,而提供了抓換機制(LLVM IR),把高級別的語言(C,C++和Rust)編譯為WebAssembly,以便有機會在瀏覽器中運行。主要是解決目前JS語言的效率問題,設計立足點為快速,內存安全和開放。

所以它其實是一種運行機制,一種新的字節碼格式(.wasm),而不是新的語言。 

2.一些關於性能的歷史與Wasm的誕生

當人們說 WebAssembly 更快的時候,一般來講是與 JavaScript 相比而言的。

JavaScript 於 1995 年問世,它的設計初衷並不是為了執行起來快,在前 10 個年頭,它的執行速度也確實不快。緊接着,瀏覽器市場競爭開始激烈起來。被人們廣為傳播的“性能大戰”在 2008 年打響。許多瀏覽器引入了 Just-in-time 編譯器,也叫 JIT。基於 JIT 的模式,JavaScript 代碼的運行漸漸變快。正是由於這些 JIT 的引入,使得 JavaScript 的性能達到了一個轉折點,JS 代碼執行速度快了 10 倍。

 隨着性能的提升,JavaScript 可以應用到以前根本沒有想到過的領域,比如用於后端開發的 Node.js。性能的提升使得 JavaScript 的應用范圍得到很大的擴展。

但這也漸漸暴露出了 JavaScript 的問題:

  • 語法太靈活導致開發大型 Web 項目困難;
  • 性能不能滿足一些場景的需要。

針對以上兩點缺陷,近年來出現了一些 JS 的代替語言,例如:

  • 微軟的 TypeScript 通過為 JS 加入靜態類型檢查來改進 JS 松散的語法,提升代碼健壯性;
  • 谷歌的 Dart 則是為瀏覽器引入新的虛擬機去直接運行 Dart 程序以提升性能;
  • 火狐的 asm.js 則是取 JS 的子集,JS 引擎針對 asm.js 做性能優化。

以上嘗試各有優缺點,其中:

  • TypeScript 只是解決了 JS 語法松散的問題,最后還是需要編譯成 JS 去運行,對性能沒有提升;
  • Dart 只能在 Chrome 預覽版中運行,無主流瀏覽器支持,用 Dart 開發的人不多;
  • asm.js 語法太簡單、有很大限制,開發效率低。

三大瀏覽器巨頭分別提出了自己的解決方案,互不兼容,這違背了 Web 的宗旨; 是技術的規范統一讓 Web 走到了今天,因此形成一套新的規范去解決 JS 所面臨的問題迫在眉睫。

於是 WebAssembly 誕生了,WebAssembly 是一種新的字節碼格式,主流瀏覽器都已經支持 WebAssembly。 和 JS 需要解釋執行不同的是,WebAssembly 字節碼和底層機器碼很相似可快速裝載運行,因此性能相對於 JS 解釋執行大大提升。 也就是說 WebAssembly 並不是一門編程語言,而是一份字節碼標准,需要用高級編程語言編譯出字節碼放到 WebAssembly 虛擬機中才能運行, 瀏覽器廠商需要做的就是根據 WebAssembly 規范實現虛擬機。

在我們沒有搞清楚 JavaScript 和 WebAssembly 之間的性能差前,我們需要理解 JS 引擎所做的工作。

3. JavaScript Just-in-time (JIT) 工作原理

JavaScript 在瀏覽器中是如何運行的?

作為一個開發人員,您將JavaScript添加到頁面時,您有一個目標並遇到一個問題。

目標:你想要告訴計算機做什么

問題:你和計算機使用不通的語言。

您說的是人類的語言,計算機說的是機器語言。盡管你不認為 JavaScript 或者其他高級語言是人類語言,但事實就是這樣的。它們的設計是為了讓人們認知,不是為機器設計的。

所以JavaScript引擎的工作就是把你的人類語言轉化成機器所理解的語言。

這就像電影《降臨》中,人類和外星人的互相交流一樣。

在這部電影中,人類語言不能逐字翻譯成外星語言。他們的語言反映出兩種對世界不同的認知。人類和機器也是這樣。

所以,怎么進行翻譯呢?

在編程中,通常有兩種翻譯方法將代碼翻譯成機器語言。你可以使用解釋器或者編譯器。

使用解釋器,翻譯的過程基本上是一行一行及時生效的。

 

編譯器是另外一種工作方式,它在執行前翻譯。

 

每種翻譯方法都有利弊。

3.1 解釋器的利弊

解釋器很快的獲取代碼並且執行。您不需要在您可以執行代碼的時候知道全部的編譯步驟。因此,解釋器感覺與 JavaScript 有着自然的契合。web 開發者能夠立即得到反饋很重要。

這也是瀏覽器最開始使用 JavaScript 解釋器的原因之一。

但是實用解釋器的弊端是當你運行相同的代碼的時候。比如,你執行了一個循環。然后你就會一遍又一遍的做同樣的事情。

3.2 編譯器的利弊

編譯器則有相反的效果。在程序開始的時候,它可能需要稍微多一點的時間來了解整個編譯的步驟。但是當運行一個循環的時候他會更快,因為他不需要重復的去翻譯每一次循環里的代碼。

因為解釋器必須在每次循環訪問時不斷重新轉換代碼,作為一個可以擺脫解釋器低效率的方法,瀏覽器開始將編譯器引入。

不同的瀏覽器實現起來稍有不同,但是基本目的是相同的。他們給 JavaScript 引擎添加了一個新的部分,稱為監視器(也稱為分析器)。該監視器在 JavaScript 運行時監控代碼,並記錄代碼片段運行的次數以及使用了那些數據類型。

如果相同的代碼行運行了幾次,這段代碼被標記為 “warm”。如果運行次數比較多,就被標記為 “hot”。

被標記為 “warm” 的代碼被扔給基礎編譯器,只能提升一點點的速度。被標記為 “hot” 的代碼被扔給優化編譯器,速度提升的更多。

關於解釋器編譯器,可以讀原文 https://blog.csdn.net/chenqiuge1984/article/details/80128715

4. 編譯器如何生成匯編

理解什么是匯編,以及編譯器如何生成它,對於理解 WebAssembly 是很有幫助的。

上面說到,人和計算機打交道,就像同外星人打交道一樣。

現在來思考一下“外星人”的大腦是如何工作的——機器的“大腦”是如何對我們輸入給它的內容進行分析和理解的。

“大腦”中,有一部分負責思考——處理加法、減法或者邏輯運算。還有其他的部分分別負責短暫記憶和長期記憶的。

這些不同的部分都有自己的名字:

負責思考的部分叫做算數邏輯單元(ALU);

寄存器提供短暫記憶功能;

隨機存取存儲器(RAM)提供長期記憶功能。

機器代碼中的語句稱作指令。

那么在指令進入“大腦”以后都發生了什么呢?它們會被切分為不同的部分傳送到不同的單元進行處理。

“大腦”切分指令通過不同連接線路進行。舉個例子,“大腦”會將指令最開始的 6 比特通過管道送到 ALU 中。而 ALU 會通過 0 和 1 的位置來決定對兩個數做加法。

這串 01 串就叫做“操作碼”,它告訴了 ALU 要執行什么樣的操作。

然后“大腦”會取后面兩個連續的 3 比特 01 串來確定把哪兩個數加到一起,而這 3 比特指的是寄存器的地址。

注意看上面機器碼的注釋:“ADD R1 R2”,這對於人類來講很容易理解其含義。這就是匯編,也叫符號機器碼,它使人類也能看懂機器代碼的含義。

可以看到匯編和這台機器的機器碼之間有直接的映射關系。正是因為如此,擁有不同機器結構的計算機會有不同的匯編系統。如果你有一個機器,它有自己的內部結構,那么它就需要它所獨有的匯編語言。

從上面的分析可以知道我們進行機器碼的翻譯並不是只有一種,不同的機器有不同的機器碼,就像我們人類也說各種各樣的語言一樣,機器也“說”不同的語言。

人類和外星人之間的語言翻譯,可能會從英語、德語或中文翻譯到外星語 A 或者外星語 B。而在程序的世界里,則是從 C、C++ 或者 JAVA 翻譯到 x86 或者 ARM。

你想要從任意一個高級語言翻譯到眾多匯編語言中的一種(依賴機器內部結構),其中一種方式是創建不同的翻譯器來完成各種高級語言到匯編的映射。

這種翻譯的效率實在太低了。為了解決這個問題,大多數編譯器都會在中間多加一層。它會把高級語言翻譯到一個低層,而這個低層又沒有低到機器碼這個層級。這就是中間代碼( intermediate representation,IR)。

這就是說編譯器會把高級語言翻譯到 IR 語言,而編譯器另外的部分再把 IR 語言編譯成特定目標結構的可執行代碼。

重新總結一下:編譯器的前端把高級語言翻譯到 IR,編譯器的后端把 IR 翻譯成目標機器的匯編代碼。

5.WebAssembly 的工作原理。

WebAssembly 是除了 JavaScript 以外,另一種可以在網頁中運行的編程語言。過去如果你想在瀏覽器中運行代碼來對網頁中各種元素進行控制,只有 JavaScript 這一種選擇。

所以當人們談論 WebAssembly 的時候,往往會拿 JavaScript 來進行比較。但是它們其實並不是“二選一”的關系——並不是只能用 WebAssembly 或者 JavaScript。

實際上,我們鼓勵開發者將這兩種語言一起使用,即使你不親自實現 WebAssembly 模塊,你也可以學習它現有的模塊,並它的優勢來實現你的功能。

WebAssembly 模塊定義的一些功能可以通過 JavaScript 來調用。所以就像你通過 npm 下載 lodash 模塊並通過 API 使用它一樣,未來你也可以下載 WebAssembly 模塊並且使用其提供的功能。

那么就讓我們來看一下如何開發 WebAssembly 模塊,以及如何通過 JavaScript 使用他們。

5.1 WebAssembly 處於哪個環節?

上面說到編譯器是如何從高級語言翻譯到機器碼的。

那么在上圖中,WebAssembly 在什么位置呢?實際上,你可以把它看成另一種“目標匯編語言”。

每一種目標匯編語言(x86、ARM)都依賴於特定的機器結構。當你想要把你的代碼放到用戶的機器上執行的時候,你並不知道目標機器結構是什么樣的。

而 WebAssembly 與其他的匯編語言不一樣,它不依賴於具體的物理機器。可以抽象地理解成它是概念機器的機器語言,而不是實際的物理機器的機器語言

正因為如此,WebAssembly 指令有時也被稱為虛擬指令。它比 JavaScript 代碼更直接地映射到機器碼,它也代表了“如何能在通用的硬件上更有效地執行代碼”的一種理念。所以它並不直接映射成特定硬件的機器碼。

 

瀏覽器把 WebAssembly 下載下來后,可以迅速地將其轉換成機器匯編代碼。

5.2 編譯到 .wasm 文件

目前對於 WebAssembly 支持情況最好的編譯器工具鏈是 LLVM。有很多不同的前端和后端插件可以用在 LLVM 上。

提示:很多 WebAssembly 開發者用 C 語言或者 Rust 開發,再編譯成 WebAssembly。其實還有其他的方式來開發 WebAssembly 模塊。例如利用 TypeScript 開發 WebAssembly 模塊,或者直接用文本格式的 WebAssembly 也可以。

假設想從 C 語言到 WebAssembly,我們就需要 clang 前端來把 C 代碼變成 LLVM 中間代碼。當變換成了 LLVM IR 時,說明 LLVM 已經理解了代碼,它會對代碼自動地做一些優化。

為了從 LLVM IR生成 WebAssembly,還需要后端編譯器。在 LLVM 的工程中有正在開發中的后端,而且應該很快就開發完成了,現在這個時間節點,暫時還看不到它是如何起作用的。

還有一個易用的工具,叫做 Emscripten。它通過自己的后端先把代碼轉換成自己的中間代碼(叫做 asm.js),然后再轉化成 WebAssembly。實際上它背后也是使用的 LLVM。

Emscripten 還包含了許多額外的工具和庫來包容整個 C/C++ 代碼庫,所以它更像是一個軟件開發者工具包(SDK)而不是編譯器。例如系統開發者需要文件系統以對文件進行讀寫,Emscripten 就有一個 IndexedDB 來模擬文件系統。

關於工具鏈,可移步另一篇文章Windows10下WebAssembly C/C++編譯環境的搭建與Hello World嘗試,只要知道最終生成了 .wasm 文件就可以了。后面我會介紹 .wasm 文件的結構,在這之前先一起了解一下在 JS 中如何使用它。

5.3 加載一個 .wasm 模塊到 JavaScript

.wasm 文件是 WebAssembly 模塊,它可以加載到 JavaScript 中使用,現階段加載的過程稍微有點復雜。

1 function fetchAndInstantiate(url, importObject) {
2   return fetch(url).then(response =>
3     response.arrayBuffer()
4   ).then(bytes =>
5     WebAssembly.instantiate(bytes, importObject)
6   ).then(results =>
7     results.instance
8   );
9 }

 

如果想深入了解,可以在  MDN 文檔中了解更多。

我們一直在致力於把這一過程變得簡單,對工具鏈進行優化。希望能夠把它整合到現有的模塊打包工具中,比如 webpack 中,或者整合到加載器中,比如 SystemJS 中。我們相信加載 WebAssembly 模塊也可以像加載 JavaScript 一樣簡單。

這里介紹 WebAssembly 模塊和 JavaScript 模塊的主要區別。當前的 WebAssembly 只能使用數字(整型或者浮點型)作為參數或者返回值。

對於任何其他的復雜類型,比如 string,就必須得用 WebAssembly 模塊的內存操作了。如果是經常使用 JavaScript,對直接操作內存不是很熟悉的話,可以回想一下 C、C++ 和 Rust 這些語言,它們都是手動操作內存。WebAssembly 的內存操作和這些語言的內存操作很像。

為了實現這個功能,它使用了 JavaScript 中稱為 ArrayBuffer 的數據結構。ArrayBuffer 是一個字節數組,所以它的索引(index)就相當於內存地址了。

如果你想在 JavaScript 和 WebAssembly 之間傳遞字符串,可以利用 ArrayBuffer 將其寫入內存中,這時候 ArrayBuffer 的索引就是整型了,可以把它傳遞給 WebAssembly 函數。此時,第一個字符的索引就可以當做指針來使用。

這就好像一個 web 開發者在開發 WebAssembly 模塊時,把這個模塊包裝了一層外衣。這樣其他使用者在使用這個模塊的時候,就不用關心內存管理的細節。

如果你想了解更多的內存管理,看一下我們寫的 WebAssembly 的內存操作

5.4 .wasm 文件結構

如果你是寫高級語言的開發者,並且通過編譯器編譯成 WebAssembly,那你不用關心 WebAssembly 模塊的結構。但是了解它的結構有助於你理解一些基本問題。

這段代碼是即將生成 WebAssembly 的 C 代碼:

1 int add42(int num) {
2     return num + 42;
3 }

 

你可以使用 WASM Explorer 來編譯這個函數。

打開 .wasm 文件(假設你的編輯器支持的話),可以看到下面代碼:

1 00 61 73 6D 0D 00 00 00 01 86 80 80 80 00 01 60
2 01 7F 01 7F 03 82 80 80 80 00 01 00 04 84 80 80
3 80 00 01 70 00 00 05 83 80 80 80 00 01 00 01 06
4 81 80 80 80 00 00 07 96 80 80 80 00 02 06 6D 65
5 6D 6F 72 79 02 00 09 5F 5A 35 61 64 64 34 32 69
6 00 00 0A 8D 80 80 80 00 01 87 80 80 80 00 00 20
7 00 41 2A 6A 0B

這是模塊的“二進制”表示。之所以用引號把“二進制”引起來,是因為上面其實是用十六進制表示的,不過把它變成二進制或者人們能看懂的十進制表示也很容易。

例如,下面是 num + 42 的各種表示方法。

5.5 代碼是如何工作的:基於棧的虛擬機

如果你對具體的操作過程很好奇,那么這幅圖可以告訴你指令都做了什么。

從圖中我們可以注意到 加 操作並沒有指定哪兩個數字進行加。這是因為 WebAssembly 是采用“基於棧的虛擬機”的機制。即一個操作符所需要的所有值,在操作進行之前都已經存放在堆棧中。

所有的操作符,比如加法,都知道自己需要多少個值。加需要兩個值,所以它從堆棧頂部取兩個值就可以了。那么加指令就可以變的更短(單字節),因為指令不需要指定源寄存器和目的寄存器。這也使得 .wasm 文件變得更小,進而使得加載 .wasm 文件更快。

盡管 WebAssembly 使用基於棧的虛擬機,但是並不是說在實際的物理機器上它就是這么生效的。當瀏覽器翻譯 WebAssembly 到機器碼時,瀏覽器會使用寄存器,而 WebAssembly 代碼並不指定用哪些寄存器,這樣做的好處是給瀏覽器最大的自由度,讓其自己來進行寄存器的最佳分配。

5.6 WebAssembly 模塊的組成部分

除了上面介紹的,.wasm 文件還有其他部分。一些組成部分對於模塊來講是必須的,一些是可選的。

必須部分:

  1. Type。在模塊中定義的函數的函數聲明和所有引入函數的函數聲明。
  2. Function。給出模塊中每個函數一個索引。
  3. Code。模塊中每個函數的實際函數體。

可選部分:

  1. Export。使函數、內存、表(tables)、全局變量等對其他 WebAssembly 或 JavaScript 可見,允許動態鏈接一些分開編譯的組件,即 .dll 的WebAssembly 版本。
  2. Import。允許從其他 WebAssembly 或者 JavaScript 中導入指定的函數、內存、表或者全局變量。
  3. Start。當 WebAssembly 模塊加載進來的時候,可以自動運行的函數(類似於 main 函數)。
  4. Global。聲明模塊的全局變量。
  5. Memory。定義模塊用到的內存。
  6. Table。使得可以映射到 WebAssembly 模塊以外的值,如映射到 JavaScript 的對象。這在間接函數調用時很有用。
  7. Data。初始化導入的或者局部內存。
  8. Element。初始化導入的或者局部的表。

如果你想了解關於這些組成部分的更深入的內容,可以閱讀這些組成部分的工作原理

6 為什么 WebAssembly 更快?

在我們了解 JavaScript 和 WebAssembly 的性能區別之前,需要先理解 JS 引擎的工作原理。

這張圖大致給出了現在一個程序的啟動性能,目前 JIT 編譯器在瀏覽器中很常見。

JS 引擎在圖中各個部分所花的時間取決於頁面所用的 JavaScript 代碼。圖表中的比例並不代表真實情況下的確切比例情況。

 

 

圖中的每一個顏色條都代表了不同的任務:

  • Parsing——表示把源代碼變成解釋器可以運行的代碼所花的時間;
  • Compiling + optimizing——表示基線編譯器和優化編譯器花的時間。一些優化編譯器的工作並不在主線程運行,不包含在這里。
  • Re-optimizing——當 JIT 發現優化假設錯誤,丟棄優化代碼所花的時間。包括重優化的時間、拋棄並返回到基線編譯器的時間。
  • Execution——執行代碼的時間。
  • Garbage collection——垃圾回收,清理內存的時間。

這里注意:這些任務並不是離散執行的,或者按固定順序依次執行的。而是交叉執行,比如正在進行解析過程時,其他一些代碼正在運行,而另一些正在編譯。

這樣的交叉執行給早期 JavaScript 帶來了很大的效率提升,早期的 JavaScript 執行類似於下圖,各個過程順序進行:

 

 早期時,JavaScript 只有解釋器,執行起來非常慢。當引入了 JIT 后,大大提升了執行效率,縮短了執行時間。

 JIT 所付出的開銷是對代碼的監視和編譯時間。JavaScript 開發者可以像以前那樣開發 JavaScript 程序,而同樣的程序,解析和編譯的時間也大大縮短。這就使得開發者們更加傾向於開發更復雜的 JavaScript 應用。

 同時,這也說明了執行效率上還有很大的提升空間。

6.1 WebAssembly 對比

下面是 WebAssembly 和典型的 web 應用的近似對比圖:

各種瀏覽器處理上圖中不同的過程,有着細微的差別,拿 SpiderMonkey 作為例子。

6.2 文件獲取

 這一步並沒有顯示在圖表中,但是這看似簡單地從服務器獲取文件這個步驟,卻會花費很長時間。

WebAssembly 比 JavaScript 的壓縮率更高,所以文件獲取也更快。即便通過壓縮算法可以顯著地減小 JavaScript 的包大小,但是壓縮后的 WebAssembly 的二進制代碼依然更小。

這就是說在服務器和客戶端之間傳輸文件更快,尤其在網絡不好的情況下。

6.3 解析

 當到達瀏覽器時,JavaScript 源代碼就被解析成了抽象語法樹。

瀏覽器采用懶加載的方式進行,只解析真正需要的部分,而對於瀏覽器暫時不需要的函數只保留它的樁(stub,譯者注:關於樁的解釋可以在之前的文章中有提及)。

解析過后 AST (抽象語法樹)就變成了中間代碼(叫做字節碼),提供給 JS 引擎編譯。

而 WebAssembly 則不需要這種轉換,因為它本身就是中間代碼。它要做的只是解碼並且檢查確認代碼沒有錯誤就可以了。

 

 

6.4 編譯和優化

 在關於 JIT 的文章中,我有介紹過,JavaScript 是在代碼的執行階段編譯的。因為它是弱類型語言,當變量類型發生變化時,同樣的代碼會被編譯成不同版本。

 不同瀏覽器處理 WebAssembly 的編譯過程也不同,有些瀏覽器只對 WebAssembly 做基線編譯,而另一些瀏覽器用 JIT 來編譯。

 不論哪種方式,WebAssembly 都更貼近機器碼,所以它更快,使它更快的原因有幾個:

  1. 在編譯優化代碼之前,它不需要提前運行代碼以知道變量都是什么類型。
  2. 編譯器不需要對同樣的代碼做不同版本的編譯。
  3. 很多優化在 LLVM 階段就已經做完了,所以在編譯和優化的時候沒有太多的優化需要做。

 

6.5 重優化

 有些情況下,JIT 會反復地進行“拋棄優化代碼<->重優化”過程。

 當 JIT 在優化假設階段做的假設,執行階段發現是不正確的時候,就會發生這種情況。比如當循環中發現本次循環所使用的變量類型和上次循環的類型不一樣,或者原型鏈中插入了新的函數,都會使 JIT 拋棄已優化的代碼。

反優化過程有兩部分開銷。第一,需要花時間丟掉已優化的代碼並且回到基線版本。第二,如果函數依舊頻繁被調用,JIT 可能會再次把它發送到優化編譯器,又做一次優化編譯,這是在做無用功。

在 WebAssembly 中,類型都是確定了的,所以 JIT 不需要根據變量的類型做優化假設。也就是說 WebAssembly 沒有重優化階段。

6.6 執行

自己也可以寫出執行效率很高的 JavaScript 代碼。你需要了解 JIT 的優化機制,例如你要知道什么樣的代碼編譯器會對其進行特殊處理(JIT 文章里面有提到過)。

然而大多數的開發者是不知道 JIT 內部的實現機制的。即使開發者知道 JIT 的內部機制,也很難寫出符合 JIT 標准的代碼,因為人們通常為了代碼可讀性更好而使用的編碼模式,恰恰不合適編譯器對代碼的優化。

加之 JIT 會針對不同的瀏覽器做不同的優化,所以對於一個瀏覽器優化的比較好,很可能在另外一個瀏覽器上執行效率就比較差。

正是因為這樣,執行 WebAssembly 通常會比較快,很多 JIT 為 JavaScript 所做的優化在 WebAssembly 並不需要。另外,WebAssembly 就是為了編譯器而設計的,開發人員不直接對其進行編程,這樣就使得 WebAssembly 專注於提供更加理想的指令(執行效率更高的指令)給機器就好了。

執行效率方面,不同的代碼功能有不同的效果,一般來講執行效率會提高 10% - 800%。

6.7 垃圾回收

JavaScript 中,開發者不需要手動清理內存中不用的變量。JS 引擎會自動地做這件事情,這個過程叫做垃圾回收。

可是,當你想要實現性能可控,垃圾回收可能就是個問題了。垃圾回收器會自動開始,這是不受你控制的,所以很有可能它會在一個不合適的時機啟動。目前的大多數瀏覽器已經能給垃圾回收安排一個合理的啟動時間,不過這還是會增加代碼執行的開銷。

目前為止,WebAssembly 不支持垃圾回收。內存操作都是手動控制的(像 C、C++一樣)。這對於開發者來講確實增加了些開發成本,不過這也使代碼的執行效率更高。

 

6.8 總結

WebAssembly 比 JavaScript 執行更快是因為:

  • 文件抓取階段,WebAssembly 比 JavaScript 抓取文件更快。即使 JavaScript 進行了壓縮,WebAssembly 文件的體積也比 JavaScript 更小;
  • 解析階段,WebAssembly 的解碼時間比 JavaScript 的解析時間更短;
  • 編譯和優化階段,WebAssembly 更具優勢,因為 WebAssembly 的代碼更接近機器碼,而 JavaScript 要先通過服務器端進行代碼優化。
  • 重優化階段,WebAssembly 不會發生重優化現象。而 JS 引擎的優化假設則可能會發生“拋棄優化代碼<->重優化”現象。
  • 執行階段,WebAssembly 更快是因為開發人員不需要懂太多的編譯器技巧,而這在 JavaScript 中是需要的。WebAssembly 代碼也更適合生成機器執行效率更高的指令。
  • 垃圾回收階段,WebAssembly 垃圾回收都是手動控制的,效率比自動回收更高。

這就是為什么在大多數情況下,同一個任務 WebAssembly 比 JavaScript 表現更好的原因。

但是,還有一些情況 WebAssembly 表現的會不如預期;同時 WebAssembly 的未來也會朝着使 WebAssembly 執行效率更高的方向發展。

7.WebAssembly 的現在與未來

 2017 年 2 月 28 日,四個主要的瀏覽器一致同意宣布 WebAssembly 的MVP 版本已經完成,它是一個瀏覽器可以搭載的穩定版本。

它提供了瀏覽器可以搭載的穩定核,這個核並沒有包含 WebAssembly 組織所計划的所有特征,而是提供了可以使 WebAssembly 穩定運行的基本版本。

這樣一來開發者就可以使用 WebAssembly 代碼了。對於舊版本的瀏覽器,開發者可以通過 asm.js 來向下兼容代碼,asm.js 是 JavaScript 的一個子集,所有 JS 引擎都可以使用它。另外,通過 Emscripten 工具,你可以把你的應用編譯成 WebAssembly 或者 asm.js。

盡管是第一個版本,WebAssembly 已經能發揮出它的優勢了,未來通過不斷地改善和融入新特征,WebAssembly 會變的更快。

7.1 提升瀏覽器中 WebAssembly 的性能

隨着各種瀏覽器都使自己的引擎支持 WebAssembly,速度提升就變成自然而然的了,目前各大瀏覽器廠商都在積極推動這件事情。

7.2 JavaScript 和 WebAssembly 之間調用的中間函數

 目前,在 JS 中調用 WebAssembly 的速度比本應達到的速度要慢。這是因為中間需要做一次“蹦床運動”。JIT 沒有辦法直接處理 WebAssembly,所以 JIT 要先把 WebAssembly 函數發送到懂它的地方。這一過程是引擎中比較慢的地方。

按理來講,如果 JIT 知道如何直接處理 WebAssembly 函數,那么速度會有百倍的提升。

如果你傳遞的是單一任務給 WebAssembly 模塊,那么不用擔心這個開銷,因為只有一次轉換,也會比較快。但是如果是頻繁地從 WebAssembly 和 JavaScript 之間切換,那么這個開銷就必須要考慮了。

7.3 快速加載

 JIT 必須要在快速加載和快速執行之間做權衡。如果在編譯和優化階段花了大量的時間,那么執行的必然會很快,但是啟動會比較慢。目前有大量的工作正在研究,如何使預編譯時間和程序真正執行時間兩者平衡。

WebAssembly 不需要對變量類型做優化假設,所以引擎也不關心在運行時的變量類型。這就給效率的提升提供了更多的可能性,比如可以使編譯和執行這兩個過程並行。

加之最新增加的 JavaScript API 允許 WebAssembly 的流編譯,這就使得在字節流還在下載的時候就啟動編譯。

FireFox 目前正在開發兩個編譯器系統。一個編譯器先啟動,對代碼進行部分優化。在代碼已經開始運行時,第二個編譯器會在后台對代碼進行全優化,當全優化過程完畢,就會將代碼替換成全優化版本繼續執行。

7.4 添加后續特性到 WebAssembly 標准的過程

WebAssembly 的發展是采用小步迭代的方式,邊測試邊開發,而不是預先設計好一切。

這就意味着有很多功能還在襁褓之中,沒有經過徹底思考以及實際驗證。它們想要寫進標准,還要通過所有的瀏覽器廠商的積極參與。

這些特性叫做:未來特性。這里列出幾個。 

直接操作 DOM

目前 WebAssembly 沒有任何方法可以與 DOM 直接交互。就是說你還不能通過比如element.innerHTML 的方法來更新節點。

 想要操作 DOM,必須要通過 JS。那么你就要在 WebAssembly 中調用 JavaScript 函數(WebAssembly 模塊中,既可以引入 WebAssembly 函數,也可以引入 JavaScript 函數)。

 

不管怎么樣,都要通過 JS 來實現,這比直接訪問 DOM 要慢得多,所以這是未來一定要解決的一個問題。

共享內存的並發性

提升代碼執行速度的一個方法是使代碼並行運行,不過有時也會適得其反,因為不同的線程在同步的時候可能會花費更多的時間。

這時如果能夠使不同的線程共享內存,那就能降低這種開銷。實現這一功能 WebAssembly 將會使用 JavaScript 中的 SharedArrayBuffer,而這一功能的實現將會提高程序執行的效率。

SIMD(單指令,多數據)

如果你之前了解過 WebAssembly 相關的內容,你可能會聽說過 SIMD,全稱是:Single Instruction, Multiple Data(單指令,多數據),這是並行化的另一種方法。

SIMD 在處理存放大量數據的數據結構有其獨特的優勢。比如存放了很多不同數據的 vector(容器),就可以用同一個指令同時對容器的不同部分做處理。這種方法會大幅提高復雜計算的效率,比如游戲或者 VR。

這對於普通 web 應用開發者不是很重要,但是對於多媒體、游戲開發者非常關鍵。

異常處理

許多語言都仿照 C++ 式的異常處理,但是 WebAssembly 並沒有包含異常處理。

如果你用 Emscripten 編譯代碼,就知道它會模擬異常處理,但是這一過程非常之慢,慢到你都想用“DISABLEEXCEPTIONCATCHING” 標記把異常處理關掉。

如果異常處理加入到了 WebAssembly,那就不用采用模擬的方式了。而異常處理對於開發者來講又特別重要,所以這也是未來的一大功能點。

其他改進——使開發者開發起來更簡單

一些未來特性不是針對性能的,而是使開發者開發 WebAssembly 更方便。

  • 一流的開發者工具。目前在瀏覽器中調試 WebAssembly 就像調試匯編一樣,很少的開發者可以手動地把自己的源代碼和匯編代碼對應起來。我們在致力於開發出更加適合開發者調試源代碼的工具。
  • 垃圾回收。如果你能提前確定變量類型,那就可以把你的代碼變成 WebAssembly,例如 TypeScript 代碼就可以編譯成 WebAssembly。但是現在的問題是 WebAssembly 沒辦法處理垃圾回收的問題,WebAssembly 中的內存操作都是手動的。所以 WebAssembly 會考慮提供方便的 GC 功能,以方便開發者使用。

  • ES6 模塊集成。目前瀏覽器在逐漸支持用 script 標記來加載 JavaScript 模塊。一旦這一功能被完美執行,那么像

8. 參考文章

陳秋歌—WebAssembly 系列六部曲

明非—圖說 WebAssembly

link—來談談WebAssembly是個啥?為何說它會影響每一個Web開發者

吳浩麟—WebAssembly 現狀與實戰

阮一峰—技術的熱門度曲線

MDN web docs—WebAssembly

WebAssembly中文網


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM