首頁>Club>
7
回覆列表
  • 1 # 武漢朝夕教育科技

    為什麼需要 WebAssembly

    自從 JavaScript 誕生起到現在已經變成最流行的程式語言,這背後正是 Web 的發展所推動的。Web 應用變得更多更復雜,但這也漸漸暴露出了 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 規範實現虛擬機器。

    WebAssembly 原理

    要搞懂 WebAssembly 的原理,需要先搞懂計算機的執行原理。 電子計算機都是由電子元件組成,為了方便處理電子元件只存在開閉兩種狀態,對應著 0 和 1,也就是說計算機只認識 0 和 1,資料和邏輯都需要由 0 和 1 表示,也就是可以直接裝載到計算機中執行的機器碼。 機器碼可讀性極差,因此人們透過高階語言 C、C++、Rust、Go 等編寫再編譯成機器碼。

    由於不同的計算機 CPU 架構不同,機器碼標準也有所差別,常見的 CPU 架構包括 x86、AMD64、ARM, 因此在由高階程式語言編譯成可自行程式碼時需要指定目標架構。

    WebAssembly 位元組碼是一種抹平了不同 CPU 架構的機器碼,WebAssembly 位元組碼不能直接在任何一種 CPU 架構上執行, 但由於非常接近機器碼,可以非常快的被翻譯為對應架構的機器碼,因此 WebAssembly 執行速度和機器碼接近,這聽上去非常像 Java 位元組碼。

    相對於 JS,WebAssembly 有如下優點:

    體積小:由於瀏覽器執行時只加載編譯成的位元組碼,一樣的邏輯比用字串描述的 JS 檔案體積要小很多;載入快:由於檔案體積小,再加上無需解釋執行,WebAssembly 能更快的載入並例項化,減少執行前的等待時間;相容性問題少:WebAssembly 是非常底層的位元組碼規範,制訂好後很少變動,就算以後發生變化,也只需在從高階語言編譯成位元組碼過程中做相容。可能出現相容性問題的地方在於 JS 和 WebAssembly 橋接的 JS 介面。

    每個高階語言都去實現原始碼到不同平臺的機器碼的轉換工作是重複的,高階語言只需要生成底層虛擬機器(LLVM)認識的中間語言(LLVM IR),LLVM 能實現:

    LLVM IR 到不同 CPU 架構機器碼的生成;機器碼編譯時效能和大小最佳化。

    除此之外 LLVM 還實現了 LLVM IR 到 WebAssembly 位元組碼的編譯功能,也就是說只要高階語言能轉換成 LLVM IR,就能被編譯成 WebAssembly 位元組碼,目前能編譯成 WebAssembly 位元組碼的高階語言有:

    AssemblyScript:語法和 TypeScript 一致,對前端來說學習成本低,為前端編寫 WebAssembly 最佳選擇;c\c++:官方推薦的方式,詳細使用見文件;Rust:語法複雜、學習成本高,對前端來說可能會不適應。詳細使用見文件;Kotlin:語法和 Java、JS 相似,語言學習成本低,詳細使用見文件;Golang:語法簡單學習成本低。但對 WebAssembly 的支援還處於未正式釋出階段,詳細使用見文件。

    通常負責把高階語言翻譯到 LLVM IR 的部分叫做編譯器前端,把 LLVM IR 編譯成各架構 CPU 對應機器碼的部分叫做編譯器後端; 現在越來越多的高階程式語言選擇 LLVM 作為後端,高階語言只需專注於如何提供開發效率更高的語法同時保持翻譯到 LLVM IR 的程式執行效能。

    編寫 WebAssemblyAssemblyScript 初體驗

    接下來詳細介紹如何使用 AssemblyScript 來編寫 WebAssembly,實現斐波那契序列的計算。 用 TypeScript 實現斐波那契序列計算的模組 f.ts 如下:

    在按照 AssemblyScript 提供的安裝教程成功安裝後, 再透過

    就能把以上程式碼編譯成可執行的 WebAssembly 模組。

    為了載入並執行編譯出的 f.wasm 模組,需要透過 JS 去載入並呼叫模組上的 f 函式,為此需要以下 JS 程式碼:

    以上程式碼中出現了一個新的內建型別 i32,這是 AssemblyScript 在 TypeScript 的基礎上內建的型別。 AssemblyScript 和 TypeScript 有細微區別,AssemblyScript 是 TypeScript 的子集,為了方便編譯成 WebAssembly 在 TypeScript 的基礎上加了更嚴格的型別限制, 區別如下:

    比 TypeScript 多了很多更細緻的內建型別,以最佳化效能和記憶體佔用,詳情文件;不能使用 any 和 undefined 型別,以及列舉型別;可空型別的變數必須是引用型別,而不能是基本資料型別如 string、number、boolean;函式中的可選引數必須提供預設值,函式必須有返回型別,無返回值的函式返回型別需要是 void;不能使用 JS 環境中的內建函式,只能使用 AssemblyScript 提供的內建函式。

    總體來說 AssemblyScript 比 TypeScript 又多了很多限制,編寫起來會覺得侷限性很大; 用 AssemblyScript 來寫 WebAssembly 經常會出現 tsc 編譯透過但執行 WebAssembly 時出錯的情況,這很可能就是你沒有遵守以上限制導致的;但 AssemblyScript 透過修改 TypeScript 編譯器預設配置能在編譯階段找出大多錯誤。

    AssemblyScript 的實現原理其實也藉助了 LLVM,它透過 TypeScript 編譯器把 TS 原始碼解析成 AST,再把 AST 翻譯成 IR,再透過 LLVM 編譯成 WebAssembly 位元組碼實現; 上面提到的各種限制都是為了方便把 AST 轉換成 LLVM IR。

    為什麼選 AssemblyScript 作為 WebAssembly 開發語言

    AssemblyScript 相對於 C、Rust 等其它語言去寫 WebAssembly 而言,好處除了對前端來說無額外新語言學習成本外,還有對於不支援 WebAssembly 的瀏覽器,可以透過 TypeScript 編譯器編譯成可正常執行的 JS 程式碼,從而實現從 JS 到 WebAssembly 的平滑遷移。

    接入 Webpack 構建

    任何新的 Web 開發技術都少不了構建流程,為了提供一套流暢的 WebAssembly 開發流程,接下來介紹接入 Webpack 具體步驟。

    1. 安裝以下依賴,以便讓 TS 原始碼被 AssemblyScript 編譯成 WebAssembly。

    2. 修改 webpack.config.js,加入 loader:

    3. 修改 TypeScript 編譯器配置 tsconfig.json,以便讓 TypeScript 編譯器能支援 AssemblyScript 中引入的內建型別和函式。

    4. 配置直接繼承自 assemblyscript 內建的配置檔案。

    WebAssembly 相關檔案格式

    前面提到了 WebAssembly 的二進位制檔案格式 wasm,這種格式的檔案人眼無法閱讀,為了閱讀 WebAssembly 檔案的邏輯,還有一種文字格式叫 wast; 以前面講到的計算斐波那契序列的模組為例,對應的 wast 檔案如下:

    這和組合語言非常像,裡面的 f64 是資料型別,f64.eq f64.sub f64.add 則是 CPU 指令。

    為了把二進位制檔案格式 wasm 轉換成人眼可見的 wast 文字,需要安裝 WebAssembly 二進位制工具箱WABT, 在 Mac 系統下可透過 brew install WABT 安裝,安裝成功後可以透過命令 wasm2wast f.wasm 獲得 wast;除此之外還可以透過 wast2wasm f.wast -o f.wasm 逆向轉換回去。

    WebAssembly 相關工具

    除了前面提到的 WebAssembly 二進位制工具箱,WebAssembly 社群還有以下常用工具:

    Emscripten: 能把 C、C++程式碼轉換成 wasm、asm.js;Binaryen: 提供更簡潔的 IR,把 IR 轉換成 wasm,並且提供 wasm 的編譯時最佳化、wasm 虛擬機器,wasm 壓縮等功能,前面提到的 AssemblyScript 就是基於它。

    WebAssembly JS API

    目前 WebAssembly 只能透過 JS 去載入和執行,但未來在瀏覽器中可以透過像載入 JS 那樣 <script src="f.wasm"></script> 去載入和執行 WebAssembly,下面來詳細介紹如何用 JS 調 WebAssembly。

    JS 調 WebAssembly 分為 3 大步:載入位元組碼 > 編譯位元組碼 > 例項化,獲取到 WebAssembly 例項後就可以透過 JS 去呼叫了,以上 3 步具體的操作是:

    對於瀏覽器可以透過網路請求去載入位元組碼,對於 Nodejs 可以透過 fs 模組讀取位元組碼檔案;在獲取到位元組碼後都需要轉換成 ArrayBuffer 後才能被編譯,透過 WebAssembly 透過的 JS API WebAssembly.compile 編譯後會透過 Promise resolve 一個 WebAssembly.Module,這個 module 是不能直接被呼叫的需要;在獲取到 module 後需要透過 WebAssembly.Instance API 去例項化 module,獲取到 Instance 後就可以像使用 JS 模組一個呼叫了。

    其中的第 2、3 步可以合併一步完成,前面提到的 WebAssembly.instantiate 就做了這兩個事情。

    WebAssembly 調 JS

    之前的例子都是用 JS 去呼叫 WebAssembly 模組,但是在有些場景下可能需要在 WebAssembly 模組中呼叫瀏覽器 API,接下來介紹如何在 WebAssembly 中呼叫 JS。

    WebAssembly.instantiate 函式支援第二個引數 WebAssembly.instantiate(bytes,importObject),這個 importObject 引數的作用就是 JS 向 WebAssembly 傳入 WebAssembly 中需要呼叫 JS 的 JS 模組。舉個具體的例子,改造前面的計算斐波那契序列在 WebAssembly 中呼叫 Web 中的 window.alert 函式把計算結果彈出來,為此需要改造載入 WebAssembly 模組的 JS 程式碼:

    對應的還需要修改 AssemblyScript 編寫的原始碼:、

    // 宣告從外部匯入的模組型別

    修改以上 AssemblyScript 原始碼後重新用 asc 透過命令 asc f.ts 編譯後輸出的 wast 檔案比之前多了幾行:

    多出的這部分 wast 程式碼就是在 AssemblyScript 中呼叫 JS 中傳入的模組的邏輯。

    除了以上常用的 API 外,WebAssembly 還提供一些 API,你可以透過這個 d.ts 檔案去檢視所有 WebAssembly JS API 的細節。

    不止於瀏覽器

    WebAssembly 作為一種底層位元組碼,除了能在瀏覽器中執行外,還能在其它環境執行。

    直接執行 wasm 二進位制檔案

    前面提到的 Binaryen 提供了在命令列中直接執行 wasm 二進位制檔案的工具,在 Mac 系統下透過 brew install binaryen 安裝成功後,透過 wasm-shell f.wasm 檔案即可直接執行。

    在 Node.js 中執行

    目前 V8 JS 引擎已經添加了對 WebAssembly 的支援,Chrome 和 Node.js 都採用了 V8 作為引擎,因此 WebAssembly 也可以執行在 Node.js 環境中;

    V8 JS 引擎在執行 WebAssembly 時,WebAssembly 和 JS 是在同一個虛擬機器中執行,而不是 WebAssembly 在一個單獨的虛擬機器中執行,這樣方便實現 JS 和 WebAssembly 之間的相互呼叫。

    要讓上面的例子在 Node.js 中執行,可以使用以下程式碼:

    unction toUint8Array(buf) {

    在 Nodejs 環境中執行 WebAssembly 的意義其實不大,原因在於 Nodejs 支援執行原生模組,而原生模組的效能比 WebAssembly 要好。 如果你是透過 C、Rust 去編寫 WebAssembly,你可以直接編譯成 Nodejs 可以呼叫的原生模組。

    WebAssembly 展望

    從上面的內容可見 WebAssembly 主要是為了解決 JS 的效能瓶頸,也就是說 WebAssembly 適合用於需要大量計算的場景,例如:

    在瀏覽器中處理音影片,flv.js 用 WebAssembly 重寫後效能會有很大提升;React 的 dom diff 中涉及到大量計算,用 WebAssembly 重寫 React 核心模組能提升效能。Safari 瀏覽器使用的 JS 引擎 JavaScriptCore 也已經支援 WebAssembly,RN 應用效能也能提升;突破大型 3D 網頁遊戲效能瓶頸,白鷺引擎已經開始探索用 WebAssembly。

    總結

    WebAssembly 標準雖然已經定稿並且得到主流瀏覽器的實現,但目前還存在以下問題:

    瀏覽器相容性不好,只有最新版本的瀏覽器支援,並且不同的瀏覽器對 JS WebAssembly 互調的 API 支援不一致;生態工具不完善不成熟,目前還不能找到一門體驗流暢的編寫 WebAssembly 的語言,都還處於起步階段;學習資料太少,還需要更多的人去探索去踩坑。;

    總之現在的 WebAssembly 還不算成熟,如果你的團隊沒有不可容忍的效能問題,那現在使用 WebAssembly 到產品中還不是時候, 因為這可能會影響到團隊的開發效率,或者遇到無法輕易解決的坑而阻塞開發。

  • 中秋節和大豐收的關聯?
  • 大棚種植黃瓜,易發生病蟲害,要如何防治呢?