發布日期:2025 年 1 月 30 日
許多網頁上的 WebAssembly 應用程式都能從多執行緒中受益,這點與原生應用程式相同。多個執行緒可讓更多工作同時進行,並將繁重的工作移出主執行緒,以避免延遲問題。直到最近,這類多執行緒應用程式才會出現一些常見的痛點,與配置和 I/O 相關。幸運的是,Emscripten 近期推出的功能可有效解決這些問題。本指南將說明這些功能如何在某些情況下,將速度提升 10 倍以上。
資源調度
下圖顯示純數學工作負載中,有效的多執行緒縮放功能 (取自我們在本文中使用的基準測試):
這項指標會評估純粹的運算作業,也就是各個 CPU 核心可自行執行的作業,因此效能會隨著核心數量增加而提升。這種成效越高則越低的下降線,正是良好的調整規模方式。這也顯示,儘管網頁平台使用網頁工作者做為平行處理的基礎,使用 Wasm 而非真正的原生程式碼,以及其他可能不太理想的細節,但仍能順利執行多執行緒原生程式碼。
堆管理:malloc
/free
malloc
和 free
是所有線性記憶體語言 (例如 C、C++、Rust 和 Zig) 中的重要標準程式庫函式,用於管理非完全靜態或位於堆疊上的所有記憶體。Emscripten 預設使用 dlmalloc
,這是一種精簡但高效的實作方式 (也支援 emmalloc
,這種方式更精簡,但在某些情況下速度較慢)。不過,dlmalloc
的多執行緒效能受到限制,因為它會鎖定每個 malloc
/free
(因為有單一全域分配器)。因此,如果您在許多執行緒中同時進行許多配置,就可能會發生爭用和速度變慢的問題。執行 malloc
密集的基準測試時,會發生以下情況:
不僅效能不會因核心數增加而提升,反而會越來越糟,因為每個執行緒都會長時間等待 malloc
鎖定。這是最糟糕的情況,但如果有足夠的配置,實際工作負載可能會發生這種情況。
mimalloc
dlmalloc
有經過多執行緒最佳化的版本,例如 ptmalloc3
,可為每個執行緒實作個別的配置器例項,避免發生爭用情形。其他幾個分配器也提供多執行緒最佳化功能,例如 jemalloc
和 tcmalloc
。Emscripten 決定專注於最近的 mimalloc
專案,這是 Microsoft 設計的分配器,具有極佳的移植性和效能。使用方式如下:
emcc -sMALLOC=mimalloc
以下是使用 mimalloc
進行 malloc
基準測試的結果:
太棒了!如今效能可有效擴展,隨著核心數增加,效能也越來越快。
如果仔細查看上一個圖表和下一個圖表中單一核心效能的資料,您會發現 dlmalloc
耗時 2660 毫秒,而 mimalloc
只耗時 1466 毫秒,速度提升了近 2 倍。這表示即使在單執行緒應用程式中,您仍可透過 mimalloc
的更精細最佳化功能獲得好處,但請注意,這會影響程式碼大小和記憶體用量 (因此 dlmalloc
仍為預設值)。
檔案和 I/O
許多應用程式都需要使用檔案,原因各有不同。例如,在遊戲中載入關卡,或在圖片編輯器中載入字型。即使是 printf
這類作業,也會在幕後使用檔案系統,因為它會透過將資料寫入 stdout
來顯示內容。
在單執行緒應用程式中,這通常不是問題,如果您只需要 printf
,Emscripten 會自動避免連結完整的檔案系統支援功能。不過,如果您使用檔案,則多執行緒檔案系統存取權會變得棘手,因為檔案存取權必須在執行緒之間同步。Emscripten 中的原始檔案系統實作項目稱為「JS FS」,因為它是使用 JavaScript 實作的,採用了只在主執行緒上實作檔案系統的簡單模型。當其他執行緒想要存取檔案時,就會將要求代理至主執行緒。這表示其他執行緒會在跨執行緒要求上遭到封鎖,而主執行緒最終會處理這項要求。
如果只有主執行緒存取檔案,這項簡單的模型就是最佳做法,這也是常見的模式。不過,如果其他執行緒執行讀取和寫入作業,就會發生問題。首先,主執行緒會為其他執行緒執行工作,導致使用者可見的延遲。接著,背景執行緒會等待主執行緒釋放,以便執行所需的工作,因此速度會變慢 (更糟的是,如果主執行緒目前正在等待該工作執行緒,可能會導致死結)。
WasmFS
為修正這個問題,Emscripten 推出了新的檔案系統實作方式:WasmFS。WasmFS 是以 C++ 編寫並編譯為 Wasm,這與原始檔案系統 (以 JavaScript 編寫) 不同。WasmFS 會將檔案儲存在 Wasm 線性記憶體中,並在所有執行緒之間共用,藉此支援多個執行緒的檔案系統存取作業,並盡可能減少額外負擔。所有執行緒現在都能以相同的效能執行檔案 I/O,而且通常還能避免彼此互相阻斷。
簡單的檔案系統基準測試顯示,與舊版 JS FS 相比,WasmFS 具有巨大優勢。
這會比較直接在主執行緒上執行的檔案系統程式碼,與在單一 pthread 上執行的程式碼。在舊版 JS FS 中,每個檔案系統作業都必須經過主執行緒的 Proxy,這會讓 pthread 的速度降低好幾個數量級!這是因為 JS FS 並非只讀取/寫入部分位元組,而是執行跨執行緒通訊,這涉及鎖定、佇列和等待。相較之下,WasmFS 可以從任何執行緒存取檔案,因此圖表顯示主執行緒和 pthread 之間幾乎沒有差異。因此,在 pthread 上,WasmFS 的速度比 JS FS 快 32 倍。
請注意,主執行緒的速度也不同,WasmFS 的速度比 2 倍 快。這是因為 JS FS 會針對每個檔案系統作業呼叫 JavaScript,而 WasmFS 會避免這類情況。WasmFS 只會在必要時 (例如使用 Web API) 使用 JavaScript,因此大部分的 WasmFS 檔案都會留在 Wasm 中。此外,即使需要 JavaScript,WasmFS 也可以使用輔助執行緒,而非主執行緒,以避免使用者可見的延遲。因此,即使應用程式不是多執行緒 (或為多執行緒,但只使用主執行緒的檔案),使用 WasmFS 仍可提升速度。
請按照下列方式使用 WasmFS:
emcc -sWASMFS
WasmFS 已在實際工作環境中使用,並被視為穩定,但尚未支援舊版 JS FS 的所有功能。另一方面,它確實包含一些重要的新功能,例如支援原始私人檔案系統 (OPFS,強烈建議用於持續性儲存空間)。除非您需要尚未移植的功能,否則 Emscripten 團隊建議使用 WasmFS。
結論
如果您有執行大量配置或使用檔案的多執行緒應用程式,使用 WasmFS 和/或 mimalloc
可能會帶來極大助益。只要使用本文所述的標記重新編譯,就能輕鬆在 Emscripten 專案中試用這兩種方法。
即使您未使用執行緒,也建議您試試這些功能:如先前所述,較新穎的實作方式會提供最佳化功能,在某些情況下,即使是單一核心,也能明顯看出差異。