用 WebAssembly 爲 Web 應用提速20倍!(案例研究)

在本文中,我們將探討如何通過用已編譯的 WebAssembly 替換 JavaScript 來加速 Web 應用。

如果你還有聽說過 WebAssembly,就先看一下解釋:WebAssembly 是一種在瀏覽器中與 JavaScript 一起運行的新語言。沒錯, JavaScript 不再是唯一在瀏覽器中運行的語言了!

除了“不是 JavaScript”之外,最大的區別是你可以將 C/C++/Rust(甚至更多!)等語言的代碼編譯爲 WebAssembly 並在瀏覽器中運行。因爲 WebAssembly 是靜態類型的,使用線性內存並以緊湊的二進制格式存儲,所以它非常快,最終可以讓我們以“接近原生”的速度運行代碼,即速度接近你通過運行二進制文件達到的速度。能夠利用現有工具和庫在瀏覽器中使用的能力以及在運行速度上的潛力,是 WebAssembly 引人注目的兩個原因。

到目前爲止,WebAssembly 已被用於各種應用,從遊戲(例如Doom 3)到把桌面程序移植到 Web(例如AutocadFigma)。它甚至可以在瀏覽器之外使用,例如 serverless 高效計算

本文是一篇用 WebAssembly 對 Web 數據分析工具進行加速的研究性案例。爲此我們用 C 編寫的已有工具執行相同的計算,並將其編譯爲 WebAssembly 來替換慢速的 JavaScript 計算。

注意本文深入研究了一些高級主題,比如編譯 C 代碼,但如果你沒有相關經驗,請不要擔心;你仍然可以繼續瞭解使用 WebAssembly 的可行性。

背景

我們將要使用的網絡應用程序是fastq.bio,這是一個交互式的網絡工具,可以讓科學家快速預覽 DNA 測序數據的質量;測序是讀取 DNA 樣品中“字母”(即核苷酸)的過程。

這是程序的截圖:
在這裏插入圖片描述

我們不會詳細討論關於計算的東西,但簡而言之,上面的圖表讓科學家們瞭解了測序的進展情況,並能夠一目瞭然地對數據的質量進行檢查。

儘管許多命令行工具都能夠生成這類質量控制報告,但 fastq.bio 的目標是在瀏覽器中提供數據質量的交互式預覽。這對於不熟悉命令行的科學家特別有用。

該應用程序的輸入是一個由測序儀器輸出的純文本文件,其中包含 DNA 序列列表和 DNA 序列中每個核苷酸的質量分數。由於該文件的格式稱爲“FASTQ”,因此網站的名稱爲 fastq.bio。

如果你對 FASTQ 格式感到好奇,請查看FASTQ的維基百科頁面。 (警告:FASTQ文件格式可能會令你不忍直視。)

Fastq.Bio:JavaScript 實現

在 fastq.bio 的原始版本中,用戶首先從計算機中選擇 FASTQ 文件。使用 File 對象,程序先從隨機位置讀取一小塊數據(使用FileReader API)。然後我們對這一大塊數據,用 JavaScript 來執行基本的字符串操作並計算相關指標。這樣的度量標準可以幫助我們跟蹤在 DNA 片段的每個位置看到的 A,C,G 和 T 的數量。

一旦該數據塊的度量標準計算完畢,我們將用 Plotly.js 以交互方式繪製結果,然後再轉到文件中的下一個塊。以小塊處理文件的原因只是爲了改善用戶體驗:一次處理整個文件需要太長時間,因爲 FASTQ 文件通常有幾百 GB。我們發現 0.5 MB 到 1 MB 之間的塊大小會使程序運行得更加流程,並且可以更及時地向用戶返回信息,但是這個數字會根據程序的具體情況和計算量的大小有所不同。

我們最開始用 JavaScript 實現的架構非常簡單:
在這裏插入圖片描述

fastq.bio 用 JavaScript 實現的體系結構:從輸入文件中隨機抽樣,用 JavaScript 計算指標並繪製結果,然後循環

紅色方框是進行字符串操作以生成指標的地方。該框是程序中計算密集度最高的那一部分,很顯然應該用 WebAssembly 對其進行運行時優化。

Fastq.Bio:WebAssembly 實現

爲了探索是否可以利用 WebAssembly 來加速 Web 應用,我們搜索了一個現成的工具來計算 FASTQ 文件的 QC 指標。具體來說,我們需要找一個用C/C++/Rust 編寫的並且已經被科學界驗證和信任得工具,然後把它移植到 WebAssembly。

經過一些研究,我們決定採用 seqtk,這是一個用 C 語言編寫的常用開源工具,可以幫我們評估測序數據的質量。

在將其編譯到 WebAssembly 之前,先讓我們研究一下怎樣將 seqtk 正常編譯爲二進制文件以便在命令行上運行。通過研究 Makefile,找到了用 gcc 進行編譯的命令:

# Compile to binary
$ gcc seqtk.c \
   -o seqtk \
   -O2 \
   -lm \
   -lz

另一方面,爲了將 seqtk 編譯爲 WebAssembly,我們需要用到 Emscripten 工具鏈,它可以直接替換現有的構建工具,使編譯 WebAssembly 的工作更容易。如果你沒有安裝 Emscripten,可以下載我們上傳到 Dockerhub 上的 docker 鏡像,該鏡像中包含了你需要的工具(你也可以從頭開始安裝,但這樣需要你花一點時間時間):

$ docker pull robertaboukhalil/emsdk:1.38.26
$ docker run -dt --name wasm-seqtk robertaboukhalil/emsdk:1.38.26

在容器內部,我們可以使用 emcc 編譯器替代 gcc

# Compile to WebAssembly
$ emcc seqtk.c \
    -o seqtk.js \
    -O2 \
    -lm \
    -s USE_ZLIB=1 \
    -s FORCE_FILESYSTEM=1

如你所見,編譯成二進制可執行文件和 WebAssembly 的方法之間的差異很小:

  1. 我們要用 Emscripten 生成一個 .wasm 和一個 .js 來對 WebAssembly 模塊進行實例化,而不是輸出一個二進制可執行文件 seqtk
  2. 爲了支持 zlib 庫,我們用了 USE_ZLIB 標誌。zlib 庫很常見,已經被移植到了 WebAssembly 中,Emscripten 會在我們的項目中包含它
  3. 我們啓用 Emscripten 的虛擬文件系統,這是一個類似 POSIX 的文件系統(源代碼),但是它只運行在瀏覽器的 RAM 中,並在刷新頁面時消失(除非你用了 IndexedDB 在瀏覽器中保存其狀態,但這不是本文所要研究的內容)。

爲什麼要啓用虛擬文件系統?要回答這個問題,先讓我們比較一下在命令行調用 seqtk 和用 JavaScript 調用已編譯的 WebAssembly 模塊這兩種方式:

# 在命令行調用
$ ./seqtk fqchk data.fastq

# 在瀏覽器控制檯中調用
> Module.callMain(["fqchk", "data.fastq"])

虛擬文件系統非常強大,因爲這意味着不必爲了處理輸入參數而重寫 seqtk 。我們可以將一塊數據作爲文件 data.fastq 掛載到虛擬文件系統上,然後簡單地調用 seqtk 的 main()函數即可。

將 seqtk 編譯爲 WebAssembly 後,得到了新的 fastq.bio 架構:
在這裏插入圖片描述

webAssembly 的體系結構和 fastW.bio 的 WebWorkers 實現:在輸入文件中隨機抽樣,用 WebAssembly 在WebWorker 中計算指標,繪製結果並循環

如圖所示,不用瀏覽器主線程而是用 WebWorkers ,這樣可以在後臺線程中執行我們的計算,並避免對瀏覽器的響應性產生負面影響。具體來說,WebWorker 控制器啓動 Worker 並管理與主線程的通信。對於 Worker,API 執行它收到的請求。

然後我們可以要求 Worker 對剛掛載的文件運行 seqtk 命令。當 seqtk 完成運行時,Worker 通過 Promise 將結果發回主線程。收到消息後,主線程用結果輸出來更新圖表。與 JavaScript 版本類似,我們用塊的形式去處理文件,並在每次循環時更新可視化圖表。

性能優化

爲了評估 WebAssembly 是否真的能夠提高運行效率,我們用每秒讀取並處理的數量作爲度量指標來比較 JavaScript 和 WebAssembly 兩種實現。在這裏忽略了生成交互式圖表所需的時間,因爲兩種實現都用了 JavaScript 來達到這一目的。

開箱即用,可以看到速度大約提升了 9 倍:
在這裏插入圖片描述

WebAssembly 與 JavaScript 實現相比,速度提升了 9 倍

這樣已經很好了,因爲它相對容易實現(前提是你理解了 WebAssembly!)。

接下來,我們注意到雖然 seqtk 輸出了許多有用的QC指標,但程序實際上並未使用或繪製了這些指標。通過剔除不需要的指標輸出,可以看到速度提高 13 倍:

在這裏插入圖片描述

刪除不必要的輸出可以進一步提高性能。

實現它是多麼的容易,這又是一個很大的改進。

最後,我們還會進一步改進。到目前爲止,fastq.bio 通過調用兩個不同的C函數來獲取感興趣的指標,每個函數計算一組不同的指標。具體做法是一個函數以直方圖的形式返回信息(即被列入範圍的值的列表),而另一個函數返回 DNA 序列位置的信息。不幸的是這意味着同一塊文件被讀取了兩次,這是沒有必要的。

所以我們把這兩個函數的代碼合併爲一個(可以不用去修改 C 代碼!)。由於兩個輸出的列數不同,我們在 JavaScript 這邊做了一些重構。這是值得的:可以讓我們得到 20 倍的速度提升!

在這裏插入圖片描述

最後,對代碼進行重構,使每個文件塊只讀取一次,這使我們的性能提高了21倍。

小心

使用 WebAssembly 時,不要期望總是獲得 20 倍的加速。如果在內存中加載非常大的文件,或者需要在 WebAssembly 和 JavaScript 之間進行大量通信,則可能會變慢。你可能只會得到 2 倍甚至是 20% 的速度。

結論

我們已經看到,通過調用編譯的 WebAssembly 來替換 JavaScript 可以使處理速度顯著增加。由於這些計算所需的代碼已經存在於 C 中,因此我們得到了重用可信工具帶來的額外好處。正如前面所提到的,WebAssembly 並不總是適合這種工作,所以還需要明智地去使用它。


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

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

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

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