2024年に100万同時タスクを実行するために必要なメモリ容量
(hez2010.github.io)ベンチマーク
-
コルーチンとは?
- コルーチンは、プログラムの実行を一時停止して再開できるコンピュータプログラムの構成要素であり、協調的マルチタスクのためのサブルーチンを一般化したもの。
- 協調処理、例外、イベントループ、イテレータ、無限リスト、パイプなどのプログラム構成要素の実装に適している。
-
Rust
- 2つのプログラムを作成:
tokioとasync_stdを使用したプログラム。 - どちらも Rust で一般的に使われる非同期ランタイム。
- 2つのプログラムを作成:
-
C#
- C# は Rust と同様に
async/awaitをサポートしている。 - .NET 7 以降では NativeAOT コンパイルが提供され、VM なしでもマネージドコードを実行できる。
- C# は Rust と同様に
-
NodeJS
- 非同期処理のために
Promise.allを使用。
- 非同期処理のために
-
Python
asyncioモジュールを使用して非同期処理を実行。
-
Go
- goroutine を使って並行性を実装し、
WaitGroupを使って処理の完了を待機する。
- goroutine を使って並行性を実装し、
-
Java
- JDK 21 以降では仮想スレッドが提供されており、これは goroutine に似た概念。
- GraalVM を使用してネイティブイメージを生成できる。
テスト環境
- ハードウェア: 13世代 Intel(R) Core(TM) i7-13700K
- OS: Debian GNU/Linux 12 (bookworm)
- Rust: 1.82.0
- .NET: 9.0.100
- Go: 1.23.3
- Java: openjdk 23.0.1
- Java (GraalVM): java 23.0.1
- NodeJS: v23.2.0
- Python: 3.13.0
結果
-
最小メモリ使用量
- Rust、C# (NativeAOT)、Go はネイティブバイナリにコンパイルされるため、少ないメモリで動作する。
- Java (GraalVM ネイティブイメージ) も良好な結果を示したが、他の静的コンパイル言語よりは多くのメモリを使用した。
-
10K タスク
- Rust はメモリ使用量がほとんど増加しない。
- C# (NativeAOT) も少ないメモリを使用する。
- Go は予想より多くのメモリを使用した。
-
100K タスク
- Rust と C# が良好な結果を示した。
- C# (NativeAOT) は Rust より少ないメモリを使用した。
-
100万タスク
- C# がすべての言語を圧倒し、最も少ないメモリを使用した。
- Rust もメモリ効率に優れている。
- Go は他の言語に比べてメモリ使用量が多い。
結論
- 多数の同時タスクは、複雑な処理を行わなくても相当なメモリを消費する可能性がある。
- .NET と NativeAOT の改善は目覚ましく、GraalVM でビルドされた Java ネイティブイメージもメモリ効率に優れている。
- goroutine は依然としてリソース消費の面で非効率的である。
付録
- Rust (
tokio) ではjoin_allの代わりにforループを使用することで、メモリ使用量を半分に削減した。今回のベンチマークでは Rust が絶対的な首位を占めた。
1件のコメント
Hacker Newsの意見
ベンチマークがNodeとGoの非同期処理方式の違いを適切に反映していない。Nodeは
Promise.allを使用し、Goはgoroutineを使用しているため差がある。非同期I/OとCPUバウンドな作業のメモリ使用量の違いを比較すると興味深いだろう「10秒間待機する作業」と「10秒後に起床する作業」の違いを説明している。Goコードのメモリ使用量が他のコードと比べて大きく異なる
GoとNodeを公平に比較するため、タイマーをスケジューリングするgoroutineと、タイマー信号を処理するgoroutineを使う方法を提案している。NodeにBunとDenoが含まれていないのは奇妙だと言及している
多数の同時作業はメモリを多く消費しうるが、作業ごとのデータが数KB以上であれば、スケジューラのメモリオーバーヘッドは無視できる程度になる
「同時作業」の定義によってメモリ使用量は変わりうる。効率的な実装では、100万の同時作業に約200MBが必要となる
Goがメモリ使用量でJavaに比べて2倍以上劣っている点を指摘し、このベンチマークは実際のプログラムを代表していないと言及している
単純なコードで言語を比較するのは開発者にとって不公平になりうるため、実際の作業を追加してメモリ使用量とスケジューリングの違いを測定することを勧めている
ベンチマークはしばしば誤りだらけであり、このようなベンチマークを投稿する人たちの動機が理解できないと述べている
Javaのベンチマークが誤っている可能性があり、
ArrayListの初期サイズを指定していないため不要なオブジェクトが大量に生成されているRustの非同期コードが予想より早く完了する理由を説明している。
tokio::time::sleep()がfutureが生成された時点を追跡しているためだ