mimalloc と WasmFS を使用したマルチスレッド WebAssembly アプリケーションのスケーリング

Alon Zakai
Alon Zakai

公開日: 2025 年 1 月 30 日

ウェブ上の多くの WebAssembly アプリケーションは、ネイティブ アプリケーションと同様にマルチスレッド処理のメリットを享受できます。複数のスレッドを使用すると、より多くの処理を並列で実行できます。また、負荷の高い処理をメインスレッドから移行して、レイテンシの問題を回避できます。最近まで、このようなマルチスレッド アプリケーションでは、割り当てと I/O に関連する一般的な問題が発生していました。幸い、Emscripten の最近の機能は、これらの問題の解決に大いに役立ちます。このガイドでは、これらの機能によって、場合によっては 10 倍以上の速度向上を実現する方法について説明します。

スケーリング

次のグラフは、純粋な数学ワークロードでの効率的なマルチスレッド スケーリングを示しています(この記事で使用するベンチマークから)。

[Math scaling] というタイトルの折れ線グラフに、コア数(X 軸)と実行時間(ミリ秒単位、Y 軸)の関係が表示されています。

これは純粋な計算を測定します。これは各 CPU コアが単独で実行できるため、コア数が増えるとパフォーマンスが向上します。パフォーマンスの向上を示すこのような下り傾向の線は、優れたスケーリングの典型的な例です。また、並列処理の基盤としてウェブワーカーを使用し、真のネイティブ コードではなく Wasm を使用し、最適化されていないと思われるその他の詳細にもかかわらず、ウェブ プラットフォームはマルチスレッド ネイティブ コードを非常にうまく実行できることを示しています。

ヒープ管理: malloc/free

mallocfree は、完全に静的ではないメモリやスタック上のメモリをすべて管理するために、すべてのリニアメモリ言語(C、C++、Rust、Zig など)で重要な標準ライブラリ関数です。Emscripten はデフォルトで dlmalloc を使用します。これはコンパクトで効率的な実装です(さらにコンパクトですが、場合によっては低速な emmalloc もサポートしています)。ただし、dlmalloc のマルチスレッド パフォーマンスは、各 malloc/free でロックを取得するため(単一のグローバル アロケータがあるため)、制限されます。したがって、多くのスレッドで同時に多くの割り当てを行うと、競合が発生してパフォーマンスが低下する可能性があります。malloc 使用量が非常に多いベンチマークを実行すると、次のような処理が行われます。

[dlmalloc スケーリング] というタイトルの折れ線グラフには、コア数(x 軸)と実行時間(ミリ秒単位、y 軸、低いほど良い)の関係が表示されます。この傾向は、コア数を増やすと実行時間が長くなることを示しています。1 コアから 4 コアに増やすと、実行時間は直線的に増加します。

コア数を増やしてもパフォーマンスは向上せず、各スレッドが malloc ロックを長時間待機するため、パフォーマンスは悪化します。これは最悪のケースですが、十分な割り当てがある場合、実際のワークロードで発生する可能性があります。

mimalloc

dlmalloc には、マルチスレッド用に最適化されたバージョン(ptmalloc3 など)があり、スレッドごとに個別のアロケータ インスタンスを実装して競合を回避します。jemalloctcmalloc など、マルチスレッド最適化を備えた他にもいくつかの割り当てツールがあります。Emscripten は、最近の mimalloc プロジェクトに重点を置くことにしました。これは、優れたポータビリティとパフォーマンスを備えた Microsoft の優れた設計のアロケータです。次のように使用します。

emcc -sMALLOC=mimalloc

mimalloc を使用した malloc ベンチマークの結果は次のとおりです。

[mimalloc スケーリング] というタイトルの折れ線グラフには、コア数(x 軸)と実行時間(ミリ秒単位、y 軸、低いほど良い)の関係が表示されます。この傾向は、コア数を増やすと実行時間が短縮されることを示しています。1 コアから 2 コアに増やすと急激に短縮され、2 コアから 4 コアに増やすとより緩やかに短縮されます。

これで終了です。パフォーマンスが効率的にスケーリングされ、コア数が増えるにつれて高速化されます。

最後の 2 つのグラフのシングルコア パフォーマンスのデータを詳しく見ると、dlmalloc は 2, 660 ms かかり、mimalloc は 1, 466 ms しかかかっていません。速度はほぼ2 倍に向上しています。これは、シングルスレッド アプリケーションでも、mimalloc のより高度な最適化によってメリットが得られることを示しています。ただし、コードサイズとメモリ使用量の増加というコストが伴うことに注意してください(このため、dlmalloc がデフォルトのままです)。

ファイルと I/O

多くのアプリケーションは、さまざまな理由でファイルを使用する必要があります。たとえば、ゲームのレベルや画像エディタのフォントを読み込む場合です。printf などのオペレーションでも、stdout にデータを書き込んで出力するため、内部でファイル システムが使用されます。

通常、シングルスレッド アプリケーションではこれは問題ではなく、必要なものが printf のみであれば、Emscripten は完全なファイル システム サポートのリンクを自動的に回避します。ただし、ファイルを使用する場合、ファイル アクセスはスレッド間で同期される必要があるため、マルチスレッド ファイル システム アクセスは複雑です。Emscripten の元のファイル システム実装(JavaScript で実装されているため「JS FS」と呼ばれます)は、メインスレッドでのみファイル システムを実装するシンプルなモデルを使用していました。別のスレッドがファイルにアクセスするたびに、メインスレッドにリクエストをプロキシします。つまり、他のスレッドはクロススレッド リクエストでブロックされ、最終的にメインスレッドが処理します。

このシンプルなモデルは、メインスレッドのみがファイルにアクセスする場合に最適です。これは一般的なパターンです。ただし、他のスレッドが読み取りと書き込みを行うと、問題が発生します。まず、メインスレッドが他のスレッドの処理を行うことになり、ユーザーに見えるレイテンシが発生します。バックグラウンド スレッドは、必要な処理を行うためにメインスレッドが空くのを待機することになり、処理が遅くなります(さらに、メインスレッドが現在そのワーカー スレッドを待機している場合は、デッドロックに陥ることもあります)。

WasmFS

この問題を解決するため、Emscripten には新しいファイル システム実装である WasmFS があります。WasmFS は C++ で記述され、Wasm にコンパイルされます。これは、JavaScript で記述された元のファイル システムとは異なります。WasmFS は、すべてのスレッド間で共有される Wasm リニアメモリにファイルを保存することで、オーバーヘッドを最小限に抑え、複数のスレッドからのファイル システム アクセスをサポートします。すべてのスレッドが同じパフォーマンスでファイル I/O を実行できるようになりました。また、多くの場合、スレッド同士のブロックを回避することもできます。

シンプルなファイル システム ベンチマークでは、古い JS FS と比較して WasmFS の大きな利点が示されています。

[ファイル システムのパフォーマンス] というタイトルの棒グラフに、JS FS と WasmFS の実行時間がミリ秒単位で比較されています(y 軸、低いほど良い)。2 つのカテゴリ(メインスレッドと pthread)が x 軸に示されています。pthread の場合、JS FS は大幅に時間がかかりますが、WasmFS はどちらの場合も低いままです。

これは、ファイル システム コードをメインスレッドで直接実行する場合と、単一の pthread で実行する場合を比較したものです。以前の JS FS では、すべてのファイル システム オペレーションをメインスレッドにプロキシする必要があり、pthread では 1 桁以上遅くなります。これは、JS FS がバイトの読み取り/書き込みを行うのではなく、ロック、キュー、待機を含むスレッド間通信を行うためです。一方、WasmFS は任意のスレッドからファイルに均等にアクセスできるため、メインスレッドと pthread の間には実質的な違いがないことがグラフからわかります。その結果、pthread の場合、WasmFS は JS FS よりも 32 倍高速です。

メイン スレッドでも違いがあり、WasmFS は2 倍高速です。これは、JS FS がファイル システム オペレーションごとに JavaScript を呼び出すためです。これは WasmFS では回避されます。WasmFS は、必要に応じてのみ JavaScript を使用します(Web API を使用する場合など)。そのため、ほとんどの WasmFS ファイルは Wasm 内に残ります。また、JavaScript が必要な場合でも、WasmFS はメインスレッドではなくヘルパー スレッドを使用できるため、ユーザーに見えるレイテンシを回避できます。このため、アプリケーションがマルチスレッドでない場合でも(またはマルチスレッドであっても、メインスレッドでのみファイルを使用する場合でも)、WasmFS を使用すると速度が向上する可能性があります。

WasmFS は次のように使用します。

emcc -sWASMFS

WasmFS は本番環境で使用され、安定していると見なされますが、古い JS FS のすべての機能をまだサポートしていません。一方、送信元のプライベート ファイル システム(OPFS、永続ストレージに強く推奨)のサポートなど、重要な新機能もいくつか含まれています。まだ移植されていない機能が必要な場合を除き、Emscripten チームは WasmFS を使用することをおすすめします。

まとめ

多くの割り当てを行うマルチスレッド アプリケーションや、ファイルを使用しているマルチスレッド アプリケーションの場合は、WasmFS や mimalloc を使用すると大きなメリットがあります。どちらも、この投稿で説明するフラグを使用して再コンパイルするだけで、Emscripten プロジェクトで簡単に試すことができます。

スレッドを使用していない場合は、これらの機能を試してみることをおすすめします。前述のように、最新の実装には、場合によってはシングルコアでも顕著な最適化が含まれています。