把 WebAssembly 用於提升速度和代碼重用

翻譯:Marty Kalin

翻譯:瘋狂的技術宅

原文:https://opensource.com/articl...

未經允許嚴禁轉載

有這樣一種技術,可以把用高級語言編寫的非 Web 程序轉換成爲 Web 準備的二進制模塊,而無需對 Web 程序的源代碼進行任何更改即可完成這種轉換。瀏覽器可以有效地下載新翻譯的模塊並在沙箱中執行。執行的 Web 模塊可以與其他 Web 技術無縫地交互 - 特別是 JavaScript(JS)。歡迎來到WebAssembly

對於名稱中帶有 assembly 的語言,WebAssembly 是低級的。但是這種低級角色鼓勵優化:瀏覽器虛擬機的即時(JIT)編譯器可以將可移植的 WebAssembly 代碼轉換爲快速的、特定於平臺的機器代碼。因此,WebAssembly 模塊成爲適用於計算綁定任務(例如數字運算)的可執行文件。

有很多高級語言都能編譯成 WebAssembly,而且這個名單正在增長,但最初的候選是C、C ++ 和 Rust。我們將這三種稱爲系統語言,因爲它們用於系統編程和高性能應用編程。系統語言都具有兩個特性,這使它們適合被編譯爲 WebAssembly。下一節將詳細介紹設置完整的代碼示例(使用 C 和 TypeScript)以及來自 WebAssembly 自己的文本格式語言的示例。

顯式數據類型和垃圾回收

這三種系統語言需要顯式數據類型,例如 intdouble,用於變量聲明和從函數返回的值。例如以下代碼段說明了 C 中的 64 位加法:

long n1 = random();
long n2 = random();
long sum = n1 + n2;

庫函數 random 聲明以 long 爲返回類型:

long random(); /* returns a long */

在編譯過程中,C 源被翻譯成彙編語言,然後再將其翻譯成機器代碼。在英特爾彙編語言(AT&T flavor)中,上面的最後一個 C 語句的功能類似以下內容(## 爲彙編語言的註釋符號):

addq %rax, %rdx ## %rax = %rax + %rdx (64-bit addition)

%rax%rdx 是 64 位寄存器,addq 指令意味着 add quadwords,其中 quadword 是 64 位大小,這是 C 語言中 long 類型的標準大小。彙編語言強調可執行機器代碼涉及類型,通過指令和參數的混合給出類型(如果有的話)。在這種情況下,add 指令是 addq(64 位加法),而不是例如 addl 這樣的指令,它增加了 C 語言典型的 int 的 32 位值。使用的寄存器字長是完整的 64 位( %rax %rdx )而不是其 32 位的(例如,%eax%rax 的低 32 位,%edx%rdx 的低 32 位)。

彙編語言的效果很好,因爲操作數被存儲在 CPU 寄存器中,而合理的 C 編譯器(即使是默認的優化級別)也會生成與此處所示相同的彙編代碼。

這三種系統語言強調顯式類型,是編譯成 WebAssembly 的理想選擇,因爲這種語言也有明確的數據類型:i32 表示 32 位的整數值,f64 表示 64 位的浮點值,依此類推。

顯式數據類型也鼓勵優化函數調用。具有顯式數據類型的函數具有 signature,它用於指定參數的數據類型以及從函數返回的值(如果有)。下面是名爲$add 的 WebAssembly 函數的簽名,該函數使用下面討論的 WebAssembly 文本格式語言編寫。該函數把兩個 32 位的整數作爲參數並返回一個 64 位的整數:

(func $add (param $lhs i32) (param $rhs i32) (result i64))

瀏覽器的 JIT 編譯器應該具有 32 位的整數參數,並把返回的 64 位值存儲在適當大小的寄存器中。

談到高性能 Web 代碼,WebAssembly 並不是唯一的選擇。例如,asm.js 是一種 JS 方言,與 WebAssembly 一樣,可以接近原生速度。 asm.js 方言允許優化,因爲代碼模仿上述三種語言中的顯式數據類型。這是 C 和 am.js 的例子。 C中的示例函數是:

int f(int n) {       /** C **/
  return n + 1;
}

參數 n 和返回值都以 int 顯式輸入。asm.js 的等效函數是:

function f(n) {      /** asm.js **/
  n = n | 0;
  return (n + 1) | 0;
}

通常,JS 沒有顯式數據類型,但 JS 中的按位或運算符能夠產生一個整數值。這就解釋了看上去毫無意義的按位或運算符:

n = n | 0;  /* bitwise-OR of n and zero */

n 和 0 之間的按位或運算得到 n,但這裏的目的是表示 n 保持整數值。 return 語句重複了這個優化技巧。

在 JS 方言中,TypeScript 在顯式數據類型方面脫穎而出,這使得這種語言對於編譯成 WebAssembly 很有吸引力。 (下面的代碼示例說明了這一點。)

三種系統語言都具有的第二個特性是它們在沒有垃圾收集器(GC)的情況下執行。對於動態分配的內存,Rust 編譯器會自動分配和釋放代碼;在其他兩種系統語言中,動態分配內存的程序員負責顯式釋放內存。系統語言避免了自動化 GC 的開銷和複雜性。

WebAssembly 的概述可以總結如下。幾乎所有關於 WebAssembly 語言的文章都提到把近乎原生的速度作爲語言的主要目標之一。 原生速度是指已編譯的系統語言的速度,因此這三種語言也是最初被指定爲編譯成 WebAssembly 的候選者的原因。

WebAssembly,JavaScript 和關注點分離

WebAssembly 語言並非爲了取代 JS,而是爲了通過在計算綁定任務上提供更好的性能來補充 JS。WebAssembly 在下載方面也有優勢。瀏覽器將 JS 模塊作爲文本提取,這正是 WebAssembly 能夠解決的低效率問題之一。WebAssembly 中的模塊是緊湊的二進制格式,可加快下載速度。

同樣令人感興趣的是 JS 和 WebAssembly 如何協同工作。 JS 旨在讀入文檔對象模型(DOM),即網頁的樹形表示。相比之下,WebAssembly 沒有爲 DOM 提供任何內置功能,但是 WebAssembly 可以導出 JS 根據需要調用的函數。這種關注點分離意味着清晰的分工:

DOM<----->JS<----->WebAssembly

無論用什麼方言,JS 都應該管理 DOM,但 JS 也可以用通過 WebAssembly 模塊提供的通用功能。代碼示例有助於說明,本文中的代碼案例可以在我的網站上找到(http://condor.depaul.edu/mkalin)。

冰雹(hailstone)序列和 Collatz 猜想

生產級代碼案例將使 WebAssembly 代碼執行繁重的計算綁定任務,例如生成大型加密密鑰對,或進行加密和解密。

考慮函數 hstone(對於hailstone),它以正整數作爲參數。該函數定義如下:

             3N + 1 if N is odd
hstone(N) =
             N/2 if N is even

例如,hstone(12) 返回 6,而 hstone(11) 返回 34。如果 N 是奇數,則 3N + 1 是偶數;但如果 N 是偶數,則 N/2 可以是偶數(例如,4/2 = 2)或奇數(例如,6/2 = 3)。

hstone 函數可以通過將返回值作爲下一個參數傳遞來進行迭代。結果是一個 hailstone 序列,例如這個序列,以 24 作爲原始參數開始,返回值 12 作爲下一個參數,依此類推:

24,12,6,3,10,5,16,8,4,2,1,4,2,1,...

序列收斂到 4,2,1 的序列無限重複需要 10 次調用:(3 x 1)+ 1 是 4,它除以 2 得 2,再除以 2 得 1。 Plus 雜誌提供了爲什麼把這些序列的稱做 hailstone 的解釋。

請注意,兩個冪很快收斂,只需要 N 除以 2 得到 1;例如,32 = 25的收斂長度爲5,64 = 26的收斂長度爲6。這裏感興趣的是從初始參數到第一個出現的序列長度。我在 C 和 TypeScript 中的代碼例子計算了冰雹序列的長度。

Collat​​z 猜想是一個冰雹序列會收斂到 1,無論初始值 N> 0 恰好是什麼。沒有人找到 Collat​​z 猜想的反例,也沒有人找到證據將猜想提升到一個定理。這個猜想很簡單,就像用程序測試一樣,是數學中一個極具挑戰性的問題。

從 C 到 WebAssembly 一步到位

下面的 hstoneCL 程序是一個非 Web 應用,可以使用常規 C 語言編譯器(例如,GNU 或 Clang)進行編譯。程序生成一個隨機整數值 N> 0 八次,並計算從 N 開始的冰雹序列的長度。兩個程序員定義的函數,mainhstone 是有意義的。該應用程序稍後會被編譯爲 WebAssembly。

示例1. C 中的 hstone 函數

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int hstone(int n) {
  int len = 0;
  while (1) {
    if (1 == n) break;           /* halt on 1 */
    if (0 == (n & 1)) n = n / 2; /* if n is even */
    else n = (3 * n) + 1;        /* if n is odd  */
    len++;                       /* increment counter */
  }
  return len;
}

#define HowMany 8

int main() {
  srand(time(NULL));  /* seed random number generator */
  int i;
  puts("  Num  Steps to 1");
  for (i = 0; i < HowMany; i++) {
    int num = rand() % 100 + 1; /* + 1 to avoid zero */
    printf("%4i %7i\n", num, hstone(num));
  }
  return 0;
}

代碼可以在任何類 Unix 系統上從命令行編譯和運行( 是命令行提示符):

% gcc -o hstoneCL hstoneCL.c  ## compile into executable hstoneCL
% ./hstoneCL                  ## execute

以下是例子運行的輸出:

  Num  Steps to 1
  88      17
   1       0
  20       7
  41     109
  80       9
  84       9
  94     105
  34      13

系統語言(包括 C)需要專門的工具鏈才能將源代碼轉換爲 WebAssembly 模塊。對於 C/C++ 語言,Emscripten 是一個開創性且仍然廣泛使用的選項,建立在衆所周知的 LLVM (低級虛擬機)編譯器基礎結構之上。我在 C 語言中的示例使用 Emscripten,你可以[使用本指南進行安裝(https://github.com/emscripten...)。

hstoneCL 程序可以通過使用 Emscription 編譯代碼進行 Web 化,而無需任何更改。Emscription工具鏈還與 JS glue(在asm.js中)一起創建一個HTML頁面,該頁面介於 DOM 和計算 hstone 函數的 WebAssembly 模塊之間。以下是步驟:

  1. 將非 Web 程序 hstoneCL 編譯到WebAssembly中:
% emcc hstoneCL.c -o hstone.html  ## generates hstone.js and hstone.wasm as well

文件 hstoneCL.c 中包含上面顯示的源代碼,-o 輸出標誌用於指定 HTML 文件的名稱。任何名稱都可以,但生成的 JS 代碼和 WebAssembly 二進制文件具有相同的名稱(在本例中,分別爲 hstone.jshstone.wasm)。較舊版本的 Emscription(在13之前)可能需要將標誌 -s WASM = 1 包含在編譯命令中。

  1. 使用 Emscription 開發 Web 服務器(或等效的)來託管 Web 化應用:
% emrun --no_browser --port 9876 .   ## . is current working directory, any port number you like

要禁止顯示警告消息,可以包含標誌 --no_emrun_detect。此命令用於啓動 Web 服務器,該服務器承載當前工作目錄中的所有資源;特別是 hstone.htmlhstone.jshstone.webasm

  1. 用支持 WebAssembly 的瀏覽器(例如,Chrome或Firefox)打開 URL http://localhost:9876/hstone.html

這個截圖顯示了我用 Firefox 運行的示例輸出。

clipboard.png

圖1. web 化 hstone 程序

結果非常顯著,因爲完整的編譯過程只需要一個命令,而且不需要對原始 C 程序進行任何更改。

微調 hstone 程序進行 Web 化

Emscription工具鏈很好地將 C 程序編譯成 WebAssembly 模塊並生成所需的 JS 膠水,但這些是機器生成的典型代碼。例如,生成的 asm.js 文件大小几乎爲 100 KB。 JS 代碼處理多個場景,並且不使用最新的 WebAssembly API。 webified hstone 程序的簡化版本將使你更容易關注 WebAssembly 模塊(位於 hstone.wasm 文件中)如何與 JS 膠水(位於 hstone.js 文件中)進行交互。

還有另一個問題:WebAssembly 代碼不需要鏡像 C 等源程序中的功能邊界。例如,C 程序 hstoneCL 有兩個用戶定義的函數,mainhstone。生成的 WebAssembly 模塊導出名爲 _ main 的函數,但不導出名爲 _ hstone 的函數。 (值得注意的是,函數 main 是 C 程序中的入口點。)C 語言 hstone 函數的主體可能在某些未導出的函數中,或者只是包含在 _ main 中。導出的 WebAssembly 函數正是 JS glue 可以通過名稱調用的函數。但是應在 WebAssembly 代碼中按名稱導出哪些源語言函數。

示例2. 修訂後的 hstone 程序

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <emscripten/emscripten.h>

int EMSCRIPTEN_KEEPALIVE hstone(int n) {
  int len = 0;
  while (1) {
    if (1 == n) break;           /* halt on 1 */
    if (0 == (n & 1)) n = n / 2; /* if n is even */
    else n = (3 * n) + 1;        /* if n is odd  */
    len++;                       /* increment counter */
  }
  return len;
}

如上所示,修改後的 hstoneWA 程序沒有 main 函數,它不再需要,因爲該程序不是作爲獨立程序運行,而是僅作爲具有單個導出函數的 WebAssembly 模塊運行。指令 EMSCRIPTEN_KEEPALIVE(在頭文件 emscripten.h 中定義)指示編譯器在 WebAssembly 模塊中導出 _ hstone 函數。命名約定很簡單:諸如 hstone 之類的 C 函數保留其名稱 —— 但在 WebAssembly 中使用單個下劃線作爲其第一個字符(在本例中爲 _ hstone)。 WebAssembly中的其他編譯器遵循不同的命名約定。

要確認此方法是否有效,可以簡化編譯步驟,僅生成 WebAssembly 模塊和 JS 粘合劑而不是 HTML:

% emcc hstoneWA.c -o hstone2.js  ## we'll provide our own HTML file

HTML文件現在可以簡化爲這個手寫的文件:

<!doctype html>
<html>
  <head>
    <meta charset="utf-8"/>
    <script src="hstone2.js"></script>
  </head>
  <body/>
</html>

HTML 文檔加載 JS 文件,後者又獲取並加載 WebAssembly 二進制文件 hstone2.wasm。順便說一下,新的 WASM 文件大小隻是原始例子的一半。

程序代碼可以像以前一樣編譯,然後使用內置的Web服務器啓動:

% emrun --no_browser --port 7777 .  ## new port number for emphasis

在瀏覽器(在本例中爲 Chrome)中請求修改後的 HTML 文檔後,可以用瀏覽器的 Web 控制檯確認 hstone 函數已導出爲 _ hstone。以下是我在 Web 控制檯中的會話段,## 爲註釋符號:

> _hstone(27)   ## invoke _hstone by name
< 111           ## output
> _hstone(7)    ## again
< 16            ## output

EMSCRIPTEN_KEEPALIVE 指令是使 Emscripten 編譯器生成 WebAssembly 模塊的簡單方法,該模塊將所有感興趣的函數導出到 JS 編程器同樣產生的 JS 粘合劑。一個自定義的 HTML 文檔,無論手寫的 JS 是否合適,都可以調用從 WebAssembly 模塊導出的函數。爲了這個乾淨的方法,向 Emscripten 致敬。

將 TypeScript 編譯爲 WebAssembly

下一個代碼示例是 TypeScript,它是具有顯式數據類型的 JS。該設置需要 Node.js 及其 npm 包管理器。以下 npm 命令安裝 AssemblyScript,它是 TypeScript 代碼的 WebAssembly 編譯器:

% npm install -g assemblyscript  ## install the AssemblyScript compiler

TypeScript 程序 hstone.ts 由單個函數組成,同樣名爲 hstone。現在數據類型如 i32(32位整數)緊跟參數和局部變量名稱(在本例中分別爲 nlen):

export function hstone(n: i32): i32 { // will be exported in WebAssembly
  let len: i32 = 0;
  while (true) {
    if (1 == n) break;            // halt on 1
    if (0 == (n & 1)) n = n / 2;  // if n is even
    else n = (3 * n) + 1;         // if n is odd
    len++;                        // increment counter
  }
  return len;
}

函數 hstone 接受一個 i32 類型的參數,並返回相同類型的值。函數的主體與 C 語言示例中的主體基本相同。代碼可以編譯成 WebAssembly,如下所示:

% asc hstone.ts -o hstone.wasm  ## compile a TypeScript file into WebAssembly

WASM 文件 hstone.wasm 的大小僅爲14 KB。

要突出顯示如何加載 WebAssembly 模塊的詳細信息,下面的手寫 HTML 文件(我的網站上找到(http://condor.depaul.edu/mkalin)中的 index.html)包含以下腳本:獲取並加載 WebAssembly 模塊 hstone.wasm 然後實例化此模塊,以便可以在瀏覽器控制檯中調用導出的 hstone 函數進行確認。

示例 3. TypeScript 代碼的 HTML頁面

<!doctype html>
<html>
  <head>
    <meta charset="utf-8"/>
    <script>
      fetch('hstone.wasm').then(response =>            <!-- Line 1 -->
      response.arrayBuffer()                           <!-- Line 2 -->
      ).then(bytes =>                                  <!-- Line 3 -->
      WebAssembly.instantiate(bytes, {imports: {}})    <!-- Line 4 -->
      ).then(results => {                              <!-- Line 5 -->
      window.hstone = results.instance.exports.hstone; <!-- Line 6 -->
      });
    </script>
  </head>
  <body/>
</html>

上面的 HTML 頁面中的腳本元素可以逐行說明。第 1 行中的 fetch 調用使用 Fetch 模塊從託管 HTML 頁面的 Web 服務器獲取 WebAssembly 模塊。當 HTTP 響應到達時,WebAssembly 模塊將把它做作爲一個字節序列,它存儲在腳本第 2 行的 arrayBuffer 中。這些字節構成了 WebAssembly 模塊,它是從 TypeScript 編譯的代碼。文件。該模塊沒有導入,如第 4 行末尾所示。

在第 4 行的開頭實例化 WebAssembly 模塊。 WebAssembly 模塊類似於非靜態類,其中包含面嚮對象語言(如Java)中的非靜態成員。該模塊包含變量、函數和各種支持組件;但是與非靜態類一樣,模塊必須實例化爲可用,在本例中是在 Web 控制檯中,但更常見的是在相應的 JS 粘合代碼中。

腳本的第 6 行以相同的名稱導出原始的 TypeScript 函數 hstone。此 WebAssembly 功能現在可用於任何 JS 粘合代碼,因爲在瀏覽器控制檯中的另一個會話將確認。

WebAssembly 具有更簡潔的 API,用於獲取和實例化模塊。新 API 將上面的腳本簡化爲 fetchinstantiate 操作。這裏展示的較長版本具有展示細節的好處,特別是將 WebAssembly 模塊表示爲字節數組,將其實例化爲具有導出函數的對象。

計劃是讓網頁以與 JS ES2015 模塊相同的方式加載 WebAssembly 模塊:

<script type='module'>...</script>

然後,JS 將獲取、編譯並以其他方式處理 WebAssembly 模塊,就像是加載另一個 JS 模塊一樣。

文本格式語言

WebAssembly 二進制文件可以轉換爲 文本格式的等價物。二進制文件通常駐留在具有 WASM 擴展名的文件中,而其人類可讀的文本副本駐留在具有 WAT 擴展名的文件中。 WABT 是一套用於處理 WebAssembly 的工具,其中包括用於轉換爲 WASM 和 WAT 格式的工具。轉換工具包括 wasm2watwasm2cwat2wasm 等。

文本格式語言採用 Lisp 推廣的 S 表達式(S for symbolic)語法。 S 表達式(簡稱 sexpr)表示把樹作爲具有任意多個子列表的列表。例如這段 sexpr 出現在 TypeScript 示例的 WAT 文件末尾附近:

(export "hstone" (func $hstone)) ## export function $hstone by the name "hstone"

樹表示是:

        export        ## root
          |
     +----+----+
     |         |
  "hstone"    func    ## left and right children
               |
            $hstone   ## single child

在文本格式中,WebAssembly 模塊是一個 sexpr,其第一項是模塊,它是樹的根。下面是一個定義和導出單個函數的模塊的簡單例子,該函數不帶參數但返回常量 9876:

(module
  (func (result i32)
    (i32.const 9876)
  )
  (export "simpleFunc" (func 0)) // 0 is the unnamed function's index
)

該函數的定義沒有名稱(即作爲 lambda),並通過引用其索引 0 導出,索引 0 是模塊中第一個嵌套的 sexpr 的索引。導出名稱以字符串形式給出;在當前情況下其名稱爲“simpleFunc”。

文本格式的函數具有標準模式,可以如下所示:

(func <signature> <local vars> <body>)

簽名指定參數(如果有)和返回值(如果有)。例如,這是一個未命名函數的簽名,它接受兩個 32 位整數參數,返回一個 64 位整數值:

(func (param i32) (param i32) (result i64)...)

名稱可以賦予函數、參數和局部變量。名稱以美元符號開頭:

(func $foo (param $a1 i32) (param $a2 f32) (local $n1 f64)...)

WebAssembly 函數的主體反映了該語言的底層棧機器體系結構。棧存儲用於暫存器。考慮一個函數的示例,該函數將其整數參數加倍並返回:

(func $doubleit (param $p i32) (result i32)
  get_local $p
  get_local $p
  i32.add)

每個 get_local 操作都可以處理局部變量和參數,將 32 位整數參數壓入棧。然後 i32.add 操作從棧中彈出前兩個(當前唯一的)值以執行添加。最後 add 操作的和是棧上的唯一值,從而成爲 $doubleit 函數的返回的值。

當 WebAssembly 代碼轉換爲機器代碼時,WebAssembly 棧作爲暫存器應儘可能由通用寄存器替換。這是 JIT 編譯器的工作,它將 WebAssembly 虛擬棧機器代碼轉換爲實際機器代碼。

Web 程序員不太可能以文本格式編寫 WebAssembly,因爲從某些高級語言編譯是一個非常有吸引力的選擇。相比之下,編譯器編的作者可能會發現在這種細粒度級別上工作是有效的。

總結

WebAssembly 的目標是實現近乎原生的速度。但隨着 JS 的 JIT 編譯器不斷改進,並且隨着非常適合優化的方言(例如,TypeScript)的出現和發展,JS 也可能實現接近原生的速度。這是否意味着 WebAssembly 是在浪費精力?我想不是。

WebAssembly 解決了計算中的另一個傳統目標:有意義的代碼重用。正如本文中的例子所示,使用適當語言(如 C 或 TypeScript)的代碼可以輕鬆轉換爲 WebAssembly 模塊,該模塊可以很好地與 JS 代碼一起使用 —— 這是連接 Web 中所使用的一系列技術的粘合劑。因此 WebAssembly 是重用遺留代碼和擴展新代碼使用的一種誘人方式。例如最初作爲桌面應用的用於圖像處理的高性能程序在 Web 應用中也可能是有用的。然後 WebAssembly 成爲重用的有吸引力的途徑。 (對於計算限制的新 Web 模塊,WebAssembly 是一個合理的選擇。)我的預感是 WebAssembly 將在重用和性能方面茁壯成長。


本文首發微信公衆號:前端先鋒

歡迎掃描二維碼關注公衆號,每天都給你推送新鮮的前端技術文章

歡迎掃描二維碼關注公衆號,每天都給你推送新鮮的前端技術文章

歡迎繼續閱讀本專欄其它高贊文章:


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章