[譯] 用 WebAssembly 提速 Web App 20 倍(例項學習)

正偉_發表於2019-04-17

原文:How We Used WebAssembly To Speed Up Our Web App By 20X (Case Study)

作者:www.smashingmagazine.com/contact/

譯文:如何使用 WebAssembly 來提高我們的 Web 應用的效能 20 倍(例項學習)

譯者:Zavier Tang


概述:在這篇文章中,我們將探索如何用編譯後的 WebAssembly 替代緩慢的 JavaScript 程式,從而提升 Web App 應用程式的速度。

如果你還沒有聽說過 WebAssembly,這是它的介紹:WebAssembly 是一種與 JavaScript 一起執行在瀏覽器中的一種新的語言。沒錯!也就是說 JavaScript 不再是唯一能在瀏覽器中執行的語言了!

但是,除了它與 JavaScript 的名稱不同之外,它的獨特之處在於,你可以從 C / C++ / Rust 等更多的語言編譯成 WebAssembly,並在瀏覽器中執行它們。因為 WebAssembly 是靜態型別的,使用線性記憶體,並以較小的二進位制格式儲存,所以它的執行速度非常快,可能接近本機原生程式的速度(即以接近於在命令列上執行二進位制程式碼的速度執行)。在瀏覽器中利用現有的工具和庫,以及相關的提速潛力,是 WebAssembly 如此強大的兩個原因。

到目前為止,WebAssembly 已被用於各種應用程式,從遊戲(如 毀滅戰士 3)到將桌面應用程式移植到 Web (如 AutocadFigma。它甚至可以在瀏覽器之外使用,例如可以作為一種用於 serverless computing 的高效且靈活的語言。

本文是一個使用 WebAssembly 加速資料分析 Web 工具的案例。為此,我們將使用一個用 C 語言編寫的可以執行相同計算的程式,將其編譯為 WebAssembly,使用它來替換緩慢的 JavaScript

注意:本文深入研究了一些高階內容,比如編譯 C 程式碼。但是如果你沒有這方面的經驗,請不要擔心,你仍然能夠一起來了解 WebAssembly 的功能。

背景介紹

我們將使用的 Web 應用程式是 fastq.bio,它是一個互動式網路工具,為科學家提供快速預覽 DNA 測序資料的質量。測序是我們讀取 DNA 樣本中的 “字母” (即核苷酸)的過程。

下面是應用程式的截圖(檢視大圖):

Interactive plots showing the user metrics for assessing the quality of their data

我們不會詳細地討論計算的細節,但是簡單地說,上圖是為科學家提供了排序進行過程的視覺化交換體驗,並被用於快速識別資料質量。

儘管有許多的命令列工具可以生成這樣的質量控制報告,但是 fastq 的目標是,在不離開瀏覽器的情況下提供資料質量的互動式預覽。對於不熟悉命令列的科學家來說,這非常有用。

應用程式的輸入是一個純文字檔案,由測序儀輸出,包含 DNA 序列列表和 DNA 序列中每個核苷酸的質量分數。該檔案的格式稱為 “FASTQ”,因此這個工具被叫做 fastq.bio

如果您對 “FASTQ” 格式感興趣,請檢視 Wikipedia page 瞭解更多。

用 JavaScript 實現 Fastq.Bio

在 fastq.bio 的原始版本中,使用者首先從計算機中選擇一個 FASTQ 檔案。使用 File 物件,應用程式從隨機的位元組位置開始讀取一小塊資料(使用 FileReader API)。在該資料塊中,我們使用 JavaScript 執行基本的字串操作並計算相關指標。其中一個度量標準幫助我們跟蹤我們通常在 DNA 片段的每個位置上看到的 A、C、G 和 T 的數量。

一旦為該資料塊計算了度量標準,我們就會用 Plotly.js 互動式地繪製結果,然後繼續處理檔案中的下一個塊。將檔案分成小塊處理的原因僅僅是為了改進使用者體驗:一次處理整個檔案將花費太長的時間,因為 FASTQ 檔案通常是幾百 GB 的。我們發現,0.5 MB 到 1 MB 之間的塊大小將使應用程式的計算更加完美,並將更快地向使用者返回資訊,但是這個數字大小會根據應用程式的細節和計算量的大小而變化。

我們最初的 JavaScript 實現的架構相當簡單:

Randomly sample from the input file, calculate metrics using JavaScript, plot the results, and loop around

fastq.bio 的 JavaScript 實現架構。(檢視大圖)

紅色的框是我們進行字串操作以生成度量的地方。該框是應用程式中計算密集型的部分,這自然使它成為使用 WebAssembly 進行執行時優化的目標。

用 WebAssembly 實現 Fastq.Bio

為了探索我們是否可以利用 WebAssembly 來加速我們的 Web 應用程式,我們搜尋了一個現成的工具來計算 FASTQ 檔案上的 QC 指標。具體地說,我們尋找了一個用 C / C++ / Rust 編寫的工具,這樣它就可以移植到 WebAssembly,而且這個工具已經得到了科學界的驗證和信任。

經過一些研究,我們決定使用 seqtk,這是一個用 C 語言編寫的常用的開源工具,可以幫助我們評估測序資料的質量(通常用於操作這些資料檔案)。

在編譯到 WebAssembly 之前,讓我們首先考慮如何將 seqtk 編譯為二進位制檔案,以便在命令列上執行它。根據生成檔案,你需要執行以下 gcc 命令:

# Compile to binary
$ gcc seqtk.c \
   -o seqtk \
   -O2 \
   -lm \
   -lz
複製程式碼

另一方面,要將 seqtk 編譯到 WebAssembly,我們可以使用 Emscripten toolchain,它為現有的構建工具提供了替換,從而使得在 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 模組:

# On the command line
$ ./seqtk fqchk data.fastq

# In the browser console
> Module.callMain(["fqchk", "data.fastq"])
複製程式碼

訪問虛擬檔案系統非常強大,因為這意味著我們不必重寫 seqtk 來處理字串輸入。我們可以在虛擬檔案系統上掛載一組資料作為檔案 data.fastq,並簡單地呼叫 seqtk 的 main() 函式。

將 seqtk 編譯到 WebAssembly 後,下面是新的 fastq.bio:

Randomly sample from the input file, calculate metrics within a WebWorker using WebAssembly, plot the results, and loop around

WebAssembly + WebWorkers 實現的 fastq.bio(檢視大圖

如圖所示,我們沒有在瀏覽器的主執行緒中執行計算,而是使用 WebWorkers,它允許我們在後臺執行緒中執行計算,從而避免對瀏覽器的響應性產生負面影響。具體來說,WebWorker 控制器啟動 Worker 並管理與主執行緒的通訊。在 Wroker 的一端,使用相關 API 執行它接收到的請求。

然後,我們可以要求 Worker 對剛剛掛載的檔案執行 seqtk 命令。當 seqtk 執行完畢時,Worker 通過一個 Promise 將結果傳送回主執行緒。一旦接收到訊息,主執行緒就將結果輸出來更新圖表。與 JavaScript 版本類似,我們以塊的形式處理檔案,並在每次迭代中更新視覺化檢視。

優化

為了評估使用 WebAssembly 是否有任何好處,我們使用每秒可以處理多少讀取資料來比較 JavaScript 和 WebAssembly。我們忽略了生成互動式圖形所花費的時間,因為這兩種實現方法都使用 JavaScript 來實現此目的。

開箱即見,有大約 9 倍的加速:

Bar chart showing that we can process 9X more lines per second

使用 WebAssembly,我們可以看到與最初的 JavaScript 實現相比,速度提高了 9 倍。(檢視大圖

這已經非常好了,因為它相對來說比較容易實現(只需要你理解了 WebAssembly 即可)。

接下來,我們注意到,雖然 seqtk 輸出了很多通常有用的 QC 指標,但我們的應用程式實際上並沒有使用或繪製這些指標。通過刪除一些我們不需要的指標的輸出,可以看到一個更大的速度,13 倍:

Bar chart showing that we can process 13X more lines per second

刪除不必要的輸出可以進一步提高效能。(檢視大圖

這又是一個很大的改進,因為很容易實現,通過註釋掉不需要的輸出語句。

最後,我們研究了另一個改進。到目前為止,fastq.bio 獲得相關度量的方法是通過兩個不同的 C 函式,每個函式計算一組不同的度量。具體來說,一個函式以直方圖的形式返回資訊(即我們放入範圍的值列表),而另一個函式以 DNA 序列位置的函式返回資訊。不幸的是,這意味著相同的檔案塊被讀取兩次,這是不必要的。

因此,我們將這兩個函式的程式碼合併到一個函式中(儘管有些混亂)。由於這兩個輸出的列數不同,所以我們在 JavaScript 端進行了一些區分,以便將它們分開。但這是值得的:這樣做讓我們實現了大於 20 倍的加速!

Bar chart showing that we can process 21X more lines per second

最後,使程式碼只讀取每個檔案塊一次可以提高大於 20 倍的效能。(檢視大圖

注意

現在是提出警告的好時機。當你使用 WebAssembly 時,不要總是期望得到 20 倍的加速。你可能只得到 2 倍的加速或者 20% 的加速。如果在記憶體中載入非常大的檔案,或者需要在 WebAssembly 和 JavaScript 之間進行大量通訊,那麼速度可能會變慢。

總結

簡而言之,我們已經看到用呼叫編譯後的 WebAssembly 來替代緩慢的 JavaScript 程式可以顯著提高速度。由於這些計算所需的程式碼已經用 C 語言實現過了,所以我們獲得了重用可信工具的額外好處。正如我們還提到的, WebAssembly 並不總是適合這項工作的工具,所以要明智地使用它。

延伸閱讀


譯者注:《JavaScript Weekly》週刊正在翻譯中...

請戳 -> JavaScript-Weekly-zh-CN

相關文章