翻譯:瘋狂的技術宅
在本文中,我們將探討如何通過用已編譯的 WebAssembly 替換 JavaScript 來加速 Web 應用。
如果你還有聽說過 WebAssembly,就先看一下解釋:WebAssembly 是一種在瀏覽器中與 JavaScript 一起執行的新語言。沒錯, JavaScript 不再是唯一在瀏覽器中執行的語言了!
除了“不是 JavaScript”之外,最大的區別是你可以將 C/C++/Rust(甚至更多!)等語言的程式碼編譯為 WebAssembly 並在瀏覽器中執行。因為 WebAssembly 是靜態型別的,使用線性記憶體並以緊湊的二進位制格式儲存,所以它非常快,最終可以讓我們以“接近原生”的速度執行程式碼,即速度接近你通過執行二進位制檔案達到的速度。能夠利用現有工具和庫在瀏覽器中使用的能力以及在執行速度上的潛力,是 WebAssembly 引人注目的兩個原因。
到目前為止,WebAssembly 已被用於各種應用,從遊戲(例如Doom 3)到把桌面程式移植到 Web(例如Autocad和 Figma )。它甚至可以在瀏覽器之外使用,例如 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 的方法之間的差異很小:
- 我們要用 Emscripten 生成一個
.wasm
和一個.js
來對 WebAssembly 模組進行例項化,而不是輸出一個二進位制可執行檔案seqtk
。 - 為了支援 zlib 庫,我們用了
USE_ZLIB
標誌。zlib 庫很常見,已經被移植到了 WebAssembly 中,Emscripten 會在我們的專案中包含它 - 我們啟用 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 並不總是適合這種工作,所以還需要明智地去使用它。