[譯] JavaScript 是如何工作的:對比 WebAssembly + 為什麼在某些場景下它比 JavaScript 更合適

stormluke發表於2018-05-23

這是專門探索 JavaScript 及其構建元件系列的第 6 期。在識別和描述核心元素的過程中,我們還分享了構建 SessionStack 時使用的一些經驗法則 —— 這是一個輕量級的 JavaScript 應用程式,但必須強大且效能卓越,才能幫助使用者實時檢視和重現其 Web 應用的缺陷。

  1. [譯] JavaScript 是如何工作的:對引擎、執行時、呼叫堆疊的概述
  2. [譯] JavaScript 是如何工作的:在 V8 引擎裡 5 個優化程式碼的技巧
  3. [譯] JavaScript 是如何工作的:記憶體管理 + 處理常見的4種記憶體洩漏
  4. [譯] JavaScript 是如何工作的: 事件迴圈和非同步程式設計的崛起 + 5個如何更好的使用 async/await 編碼的技巧
  5. [譯] JavaScript 是如何工作的:深入剖析 WebSockets 和擁有 SSE 技術 的 HTTP/2,以及如何在二者中做出正確的選擇

這次我們將剖析 WebAssembly 的工作原理,更重要的是在效能方面分析它與 JavaScript 的差異:載入時間、執行速度、垃圾回收、記憶體使用情況、平臺 API 呼叫、除錯、多執行緒和可移植性。

我們構建 Web 應用程式的方式正處於革命的邊緣 —— 仍然是初級階段,但我們對 Web 應用程式的看法正在發生變化。

首先,讓我們看看 WebAssembly 的功能

WebAssembly(也叫作 wasm)是一種高效且底層的給 web 使用的位元組碼。

WASM 讓你能夠用 JavaScript 之外的語言(例如 C、C++、Rust 或其他)編寫程式,然後將其(提前)編譯到 WebAssembly。

其結果是 Web 應用程式載入和執行速度都非常快。

載入時間

為了載入 JavaScript,瀏覽器必須載入所有文字形式的 .js 檔案。

WebAssembly 在瀏覽器中載入速度更快,因為只需通過網際網路傳輸已編譯的 wasm 檔案。而 wasm 是一種非常簡潔的二進位制格式的底層類組合語言。

執行

今天 Wasm 的執行速度只比原生程式碼(native code)執行慢 20%。無論如何,這是一個驚人的結果。這是一種編譯到沙盒環境中的格式,並且在很多約束條件下執行,以確保它沒有或者很難有安全漏洞。與真正的原生程式碼相比,速度損失很小。更重要的是,它將在未來更快

更好的是,它與瀏覽器無關 —— 目前所有主要引擎都增加了對 WebAssembly 的支援,並且執行時間相近。

為了理解 WebAssembly 與 JavaScript 相比執行得有多快,你應該首先閱讀我們關於 JavaScript 引擎的文章

我們來看看大概看看 V8 中會發生什麼:

[譯] JavaScript 是如何工作的:對比 WebAssembly + 為什麼在某些場景下它比 JavaScript 更合適

V8 的方法:延遲編譯

在左邊,我們有一些 JavaScript 原始碼,包含 JavaScript 函式。首先需要解析它,以便將所有字串轉換為詞法標記(token)並生成抽象語法樹(AST)。AST 是 JavaScript 程式邏輯的記憶體表示。一旦生成了這種表示,V8 會直接跳到機器碼。過程基本上是遍歷語法樹,生成機器程式碼,最後得到編譯好的函式。沒有真正的嘗試來加速它。

現在,我們來看看 V8 流水線在下一階段的功能:

[譯] JavaScript 是如何工作的:對比 WebAssembly + 為什麼在某些場景下它比 JavaScript 更合適

V8 流水線設計。

這次我們有了 TurboFan —— V8 的優化編譯器之一。隨著你的 JavaScript 應用的執行,大量程式碼執行在 V8 中。TurboFan 可以監控某些程式碼是否執行緩慢,是否存在瓶頸和熱點來優化它們。它把這些程式碼推到編譯器後端 —— 一個優化的 JIT,這個後端可為那些消耗大部分 CPU 的函式建立更快的程式碼。

它解決了上面的問題,但這裡的問題在於,分析並決定優化哪些程式碼的過程也會消耗 CPU。這反過來又意味著更高的電池消耗,特別是在移動裝置上。

好了,wasm 並不需要所有的這些 —— 它會被插入工作流中,如下所示:

[譯] JavaScript 是如何工作的:對比 WebAssembly + 為什麼在某些場景下它比 JavaScript 更合適

V8 流水線設計 + WASM。

Wasm 在編譯階段就已經優化好。最重要的是,也不再需要解析過程。你有了一個已優化的二進位制檔案,它可以直接掛接到生成機器碼的編譯器後端。所有優化都在編譯器前端完成。

這讓執行 wasm 更有效率,因為流程中的很多步驟都可以簡單地跳過。

記憶體模型

[譯] JavaScript 是如何工作的:對比 WebAssembly + 為什麼在某些場景下它比 JavaScript 更合適

WebAssembly 可信和不可信狀態。

舉個例子,C++ 程式中的記憶體是一個連續的區塊,其中並沒有「空隙」。有助於提高安全性的 wasm 的特性之一是,執行棧與線性記憶體分離的概念。在 C++ 程式中,你有一個堆,你從底部分配堆記憶體,並從堆頂部獲取棧空間。這就有可能造出一個指向棧空間的指標來玩弄那些本不應該接觸到的變數。

這是很多惡意軟體所利用的缺陷。

WebAssembly 採用完全不同的模型。執行棧與 WebAssembly 程式本身是分開的,因此你無法修改棧變數等內容。而且,函式中使用整數偏移而不是指標。函式指向一個間接函式表。然後通過這些計算出的直接數字跳轉到模組內部的函式中。這種設計方式使得你可以載入多個 wasm 模組,並排排列,平移所有的索引,互不影響。

有關 JavaScript 中記憶體模型和管理的更多資訊,可以檢視我們非常詳細的關於此主題的文章

垃圾回收

你已經知道 JavaScript 的記憶體管理是使用垃圾收集器處理的。

WebAssembly 的情況有點不同。它支援手動管理記憶體的語言。你的 wasm 模組可以自帶 GC,但這是一項複雜的任務。

目前,WebAssembly 是圍繞 C++ 和 RUST 用例設計的。由於 wasm 是非常底層的,因此只有組合語言上一層的程式語言才易於編譯。C 可以使用普通的 malloc,C++ 可以使用智慧指標,Rust 使用完全不同的形式(完全不同的主題)。這些語言不使用 GC,因此它們不需要那些複雜的執行時事務來跟蹤記憶體。WebAssembly 對他們來說是天作之合。

另外,這些語言並不是 100% 被設計用於呼叫複雜的 JavaScript 事物,如操作 DOM。完全在 C++ 中編寫 HTML 應用是沒有意義的,因為 C++ 不是為它設計的。在大多數情況下,當工程師編寫 C++ 或 Rust 時,他們的目標是 WebGL 或高度優化的庫(例如繁重的數學計算)。

但是,將來 WebAssembly 也將支援不附帶 GC(但需要垃圾回收)的語言。

平臺 API 呼叫

取決於執行 JavaScript 的執行時,不同特定於平臺的 API 可以通過 JavaScript 應用程式直接訪問。例如,如果在瀏覽器中執行 JavaScript,你可以通過一系列 Web APIs 來控制 web 瀏覽器 / 裝置的功能,並且可以使用例如 DOMCSSOMWebGLIndexedDBWeb Audio API 等等

好吧,WebAssembly 模組無法直接呼叫任何平臺 API。一切都是由 JavaScript 代理的。如果你想在 WebAssembly 模組中呼叫的某些平臺特定的 API,則必須通過 JavaScript 呼叫它。

例如,如果你想用 console.log,必須通過 JavaScript 呼叫它,而不是你的 C++ 程式碼。這些 JavaScript 呼叫的成本會比較高。

也並不總是如此。規範將在未來為平臺 API 提供 wasm 介面,並且你將能夠在沒有 JavaScript 的情況下發布應用程式。

原始碼對映

當你壓縮 JavaScript 程式碼時,需要一種正確除錯它的方法。這就是原始碼對映大顯身手地方。

基本上,原始碼對映是一種將整合/壓縮檔案對映回構建前狀態的方法。當你構建線上版時,壓縮和組合 JavaScript 檔案時將生成一個包含原始檔案資訊的原始碼對映。當你在生成的 JavaScript 中查詢某一行號和列號時,可以在原始碼對映中查詢程式碼的原始位置。

WebAssembly 目前不支援原始碼對映,因為暫時沒有規範,但最終會有的(可能很快)。

當在 C++ 程式碼中設定斷點時,你將看到 C++ 程式碼而不是 WebAssembly。至少這是目標。

多執行緒

JavaScript 在單執行緒上執行。有很多方法可以發揮事件迴圈和非同步程式設計優勢,詳見我們關於該主題的文章

JavaScript 也使用 Web Workers,但他們有一個非常具體的用例 —— 基本上,阻止主 UI 執行緒的任何重 CPU 計算都可以從 Web Worker 中受益。但是 Web Workers 無法訪問 DOM。

WebAssembly 目前不支援多執行緒。但是未來可能會。Wasm 將會和本地執行緒更近(例如 C++ 型執行緒)。擁有「真實」的執行緒將在瀏覽器中創造出許多新的機會。當然,這也將開啟更多濫用可能性的大門。

可移植性

如今,JavaScript 幾乎可以在任何地方執行,從瀏覽器到伺服器端甚至嵌入式系統。

WebAssembly 設計目標是安全且可移植。就像 JavaScript 一樣。它將執行在支援 wasm 的每個環境中(例如每個瀏覽器)。

WebAssembly 具有與 Java Applets 初期嘗試實現的移植性相同的可移植性目標。

在哪裡使用 WebAssembly 比 JavaScript 更好?

在 WebAssembly 的第一個版本中,主要關注 CPU 佔用大的計算(例如處理數學)。想到的最主流的用途是遊戲 —— 那裡有大量的畫素操作。你可以使用你習慣的 OpenGL 繫結在 C++ / Rust 中編寫應用,並將其編譯為 wasm。它會在瀏覽器中執行。

看看這個(在 Firefox 中執行)—— s3.amazonaws.com/mozilla-gam…。它使用虛幻引擎

另一種使用 WebAssembly 可能有意義(效能方面)的場景是實現一些這是一個 CPU 密集型的庫。例如,一些影象處理庫。

如前所述,由於大多數處理步驟都是在編譯期間提前完成的,因此 wasm 可以減少移動裝置上的電池消耗(取決於引擎)。

將來,即使你實際上沒有編寫程式碼,你也可以使用 WASM 二進位制檔案。可以在 NPM 中找到開始使用此方法的專案。

對於 DOM 操作和大量的平臺 API 操作,當然用 JavaScript 更好,因為它不會增加額外的開銷,並且具有原生的 API。

SessionStack,為了編寫高度優化且高效的程式碼,我們不斷突破 JavaScript 效能的機極限。我們的解決方案需要提供超快的效能,因為我們不能阻礙客戶應用本身。將 SessionStack 整合到線上 Web 應用或網站後,它會開始記錄所有內容:所有 DOM 更改、使用者互動、JavaScript 異常、堆疊跟蹤、失敗的網路請求和除錯資料。所有這些都在你的線上環境中進行,但不會影響產品的任何體驗和效能。我們需要大量優化我們的程式碼並儘可能使其非同步。

而且不只是庫!當你在 SessionStack 中重放使用者會話時,我們必須渲染在發生問題時使用者瀏覽器中發生的所有事件,並且必須重構整個狀態,允許你在會話時間線中來回跳轉。為了做到這一點,我們正在大量使用 JavaScript 提供的非同步能力,因為缺少更好的選擇。

藉助 WebAssembly,我們能夠將一些最繁重的處理和渲染交給更適合做這個工作的語言,同時將資料收集和 DOM 操作留給 JavaScript。

如果你想試試 SessionStack,可以從這裡免費開始。免費版可以提供 1000 會話 / 月。

[譯] JavaScript 是如何工作的:對比 WebAssembly + 為什麼在某些場景下它比 JavaScript 更合適

資源:

  • https://www.youtube.com/watch?v=6v4E6oksar0
  • https://www.youtube.com/watch?v=6Y3W94_8scw

掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章