様々なプログラミング言語の速さを測定して比較するにはどうすれば良いのか - C# / C++ / Java / Go を同条件で比べる実践ガイド
「C++ は速いらしい」 「Go は実運用で軽い」 「Java は長時間回すとかなり速い」 「C# も .NET の JIT があるから意外と強い」
この手の話はよく出ます。
ただ、ここで一番やってはいけないのは、別々の人が別々の環境で測った数字を並べて、そのまま言語の優劣だと決めることです。
C# と Java は JIT や warm-up の影響を受けやすく、C++ と Go は通常は事前コンパイル済みです。
GC の有無や特性も違います。標準ライブラリや周辺ライブラリの実装差もかなり効きます。さらに、同じマシンでも電源設定、熱、バックグラウンド処理、入力データの偏りで結果は簡単にぶれます。なかなか泥くさい世界です。
この記事では、C# / C++ / Java / Go をできるだけ公平に比較するための測り方を整理します。
結論だけ先に言うと、「どの言語が最速か」を 1 本の数字で決めようとしないことがいちばん大事です。
この記事は、あくまで比較方法の整理が主題です。
環境依存の数字をそれっぽく並べても占いっぽくなるので、ここでは実測ランキングは書きません。その代わり、どう設計すれば比較に価値が出るかに絞って整理します。
目次
- まず結論
- 最初に決めるべきこと
- 1. 起動時間を見たいのか
- 2. 長時間運転の throughput を見たいのか
- 3. tail latency を見たいのか
- 4. メモリ効率も含めて見たいのか
- なぜ言語比較は難しいのか
- JIT と AOT を混ぜると別の実験になる
- 言語差より実装差のほうが大きいことが普通にある
- C++ は最適化で処理が消える罠がある
- GC の存在は「不利」でも「有利」でもなく、特性
- 比較でやってはいけないこと
- 1. Debug と Release を混ぜる
- 2. 同じ問題を解いていない
- 3. 1 回だけ実行して結論を出す
- 4. warm-up を混ぜる
- 5. 正しさ確認をしない
- 6. 1 本の microbenchmark だけで世界観を決める
- C# / C++ / Java / Go を比べるときの基本方針
- 1. 言語内の測定は、その言語に合ったハーネスを使う
- 2. 言語横断の比較は、外側に共通ランナーを置く
- 具体例: どんなベンチ項目を用意するべきか
- おすすめの構成
- 1. sort_int32_10m
- 2. hash_group_count
- 3. parallel_sha256
- 4. startup_noop または startup_parse_small
- JSON や HTTP ベンチはどうするか
- おすすめの構成
- 言語ごとに揃えるべき条件
- C++
- C#
- Java
- Go
- 実行環境の揃え方
- 揃えるべきもの
- 特に効くもの
- 電源設定と CPU 周波数
- 熱
- バックグラウンド処理
- 何を測るべきか
- 1. wall-clock time
- 2. CPU time
- 3. memory / allocations
- 4. 分布
- 実行手順のおすすめ
- 1. workload を決める
- 2. 共通データセットを固定する
- 3. 正しさ確認を先に通す
- 4. build 条件を固定する
- 5. cold と warm を分ける
- 6. 実行順序を交互またはランダム化する
- 7. 回数を確保する
- 8. raw data を保存する
- 9. 差が出たら profile を取る
- 結果の読み方
- 初回だけ C# / Java が遅い
- C++ が tight loop で強い
- Go が起動時間や配布しやすさで有利に見える
- C# / Java が steady-state でかなり追いつく、あるいは逆転する
- allocation-heavy な処理で差が大きい
- 記録テンプレート
- まとめ
- 参考資料
- 関連トピック
- このテーマの相談先
まず結論
先に結論だけ言うと、C# / C++ / Java / Go の速度比較で本当に効くのは次の 7 つです。
-
何の速さを比べたいのかを先に決める
起動時間なのか、定常状態の throughput なのか、p95 遅延なのか、メモリ効率なのかで、測り方が変わります。 -
1 本のベンチだけで結論を出さない
CPU 計算、メモリ割り当て、並列処理、起動時間では、強い言語やランタイムの見え方が変わります。 -
C# と Java は cold と warm を分ける
初回実行を含む比較と、warm-up 後の定常状態比較を混ぜると、話がねじれます。 -
同じアルゴリズム、同じ入力、同じ正しさ確認で測る
速い実装ではなく、別の問題を解いていただけ、はベンチあるあるです。 -
言語内の microbenchmark と、言語横断の end-to-end ベンチを分ける
各言語の専用ハーネスは便利ですが、言語をまたぐ比較は外側の共通ランナーで回すほうが筋がよいです。 -
平均だけでなく中央値と分布を見る
1 回だけ GC やバックグラウンド処理が刺さるだけで平均は壊れます。 -
数字だけでなく条件を残す
ベンチ結果は速さの記録であると同時に、実験条件の記録です。条件が書かれていない結果は、後からかなりつらいです。
最初に決めるべきこと
「速い」を 1 個の言葉で済ませると、だいたい事故ります。
まずは 何を速いと呼ぶのか を決めます。
たとえば、同じプログラムでも見たいものはかなり違います。
1. 起動時間を見たいのか
CLI ツール、短命なバッチ、1 回起動してすぐ終わる補助ツールなら、cold start や process startup が効きます。
この軸では、JIT やクラスロードの初期化コストを含むかどうかで結果が大きく変わります。
2. 長時間運転の throughput を見たいのか
サーバー、常駐プロセス、ワーカー、長く動く変換処理なら、steady-state の throughput が重要です。
この場合、初回だけ遅いこと自体は本質ではなく、warm-up 後にどこまで安定して伸びるかが主題になります。
3. tail latency を見たいのか
API、UI、リアルタイム寄りの処理では、平均より p95 / p99 のほうが大事なことがあります。
平均が速くても、たまに大きく止まるなら、ユーザー体験や SLA 的にはつらいです。
4. メモリ効率も含めて見たいのか
CPU 時間だけでなく、最大 RSS、割り当て量、GC 回数、GC pause も見ないと、実運用の重さは読み違えます。
「速いけどメモリをかなり食う」と「少し遅いけど安定して軽い」は、用途によって評価が逆転します。
要するに、最初に決めるべき問いは
この比較で知りたいのは、どの言語が速いか ではなく、
どの workload を、どの条件で、どの指標において速く処理できるか
です。
ここを曖昧にしたまま数字を集めると、最後にまとまりません。
なぜ言語比較は難しいのか
JIT と AOT を混ぜると別の実験になる
C# と Java は通常 JIT の影響を受けます。
一方で、C++ と Go は通常は事前コンパイル済みです。
つまり、初回実行を測れば、プログラム本体の速さ だけでなく、ランタイムの起動・クラスロード・JIT 準備 も一緒に測ることになります。
逆に、十分に warm-up した後だけを見るなら、今度は 定常状態の最適化がどこまで効くか の比較になります。
どちらも意味はあります。
ただし、同じ意味ではありません。
言語差より実装差のほうが大きいことが普通にある
同じ「ソート」でも、
- 片方は標準ライブラリを使っている
- 片方は自前実装
- 片方は余分なコピーをしている
- 片方は入力を毎回再生成している
これだけで結果はかなり変わります。
さらに、JSON、圧縮、暗号、正規表現のような処理になると、言語そのもの より ライブラリ実装 の差がかなり効きます。
そのため、何を測っているのかを明示しないと、「言語比較」のつもりが「ライブラリ比較」になります。
C++ は最適化で処理が消える罠がある
特に microbenchmark では、コンパイラが「この計算結果、誰も使っていないな」と判断すると、処理を消してしまうことがあります。
すると、速いのではなく、そもそも何もしていない という、ちょっとした怪談になります。
C++ ではこの問題が特に露骨に出やすいので、結果の使用や checksum の出力、あるいは benchmark フレームワークの最適化抑制機能がかなり重要です。
GC の存在は「不利」でも「有利」でもなく、特性
C#、Java、Go には GC があります。
これを単純に「GC があるから遅い」とすると雑すぎます。
実際には、
- 大量短命オブジェクトをどうさばくか
- ヒープサイズ設定
- GC の頻度と pause
- オブジェクトレイアウト
- ライブラリの割り当て癖
のほうが効きます。
逆に C++ は手動管理や RAII で細かく制御できますが、そのぶん設計や実装の差が出やすいです。
つまり、管理方式の違いが、そのまま善悪や優劣ではない です。
比較でやってはいけないこと
1. Debug と Release を混ぜる
これは論外です。
比較対象は必ず 本番相当の最適化ビルド に揃えます。
2. 同じ問題を解いていない
入力形式が違う、出力が違う、エラー処理が片方だけない、メモリ再利用の方針が違う。
このへんを放置すると、速さではなく要件差 を測ってしまいます。
3. 1 回だけ実行して結論を出す
1 回だけの実行は、だいたいノイズです。
- JIT
- ページキャッシュ
- CPU のブースト
- 熱
- バックグラウンドタスク
- GC
- 初回のファイル読み込み
このへんが 1 回で全部混ざります。
4. warm-up を混ぜる
C# と Java を測るとき、初回を含めるのか、warm-up 後だけを見るのかを曖昧にすると、議論が崩れます。
cold と warm は別物 として扱います。
5. 正しさ確認をしない
ベンチは「速い」より先に「同じ結果を返す」が必要です。
比較対象の全実装で、同じ入力から同じ checksum や同じ出力 が得られることを必ず確認します。
6. 1 本の microbenchmark だけで世界観を決める
tight loop だけで勝っても、実サービス全体で勝つとは限りません。
逆に、起動時間で負けても、長時間運転では十分強いこともあります。
C# / C++ / Java / Go を比べるときの基本方針
ここはかなり重要です。
おすすめは 2 層構成 です。
1. 言語内の測定は、その言語に合ったハーネスを使う
各言語には、その言語の事情を吸収してくれる benchmark ツールがあります。
- C#: BenchmarkDotNet
- Java: JMH
- Go:
go test -benchとbenchstat - C++: Google Benchmark
これらは、それぞれのランタイム事情や統計処理、測定の罠をある程度面倒見てくれます。
言語内での比較 や 実装の掘り下げ にはかなり有効です。
2. 言語横断の比較は、外側に共通ランナーを置く
一方で、C# の BenchmarkDotNet の結果 と Java の JMH の結果 を、そのまま横に並べるのは少し危ないです。
ハーネス自体の作法が違うからです。
そのため、言語横断では、各実装を 同じ CLI 契約で呼べる実行ファイル にして、外側から同じ条件で回すのがおすすめです。
たとえば各言語で、次のような形の実行ファイルを用意します。
bench --scenario sort_int32 --dataset data/sort_10m.bin --mode warm
bench --scenario group_words --dataset data/words_100mb.txt --mode cold
bench --scenario parallel_hash --dataset data/blob_1gb.bin --threads 8
そして共通ランナー側で、
- 実行順序をランダム化する
- cold / warm を分ける
- 同じデータセットを渡す
- checksum を検証する
- wall-clock とメモリを採る
- CSV / JSON に raw data を残す
という流れにします。
これをやると、各言語の中のベストプラクティス と 言語横断の公平さ を分けて扱いやすくなります。
具体例: どんなベンチ項目を用意するべきか
「C# / C++ / Java / Go を比べたい」と言われたとき、1 本だけなら 誤解しにくい単純な CPU 系 を、複数本やるなら 性格の違う workload を 3〜4 本 用意するのがおすすめです。
おすすめの構成
1. sort_int32_10m
目的: CPU + メモリ帯域 + 一時領域の使い方を見る
- 入力: 固定 seed で生成した 1,000 万件の
int32 - 処理: 配列を sort して checksum を返す
- 注意点: 毎回同じ未ソート入力に戻すこと
これは比較的分かりやすいです。
ただし、標準ソート実装の差も含むので、言語そのもの というより 標準ライブラリ込みの比較 になります。
2. hash_group_count
目的: ハッシュテーブル、文字列処理、割り当て、GC の傾向を見る
- 入力: 固定のテキストデータ
- 処理: 単語ごとの出現回数を数える
- 出力: 上位 N 件と checksum
これは実務に近い反面、文字列ライブラリや map 実装の差もかなり効きます。
そのぶん、現実に近い比較になります。
3. parallel_sha256
目的: 並列処理、スケジューラ、ワーカープール、同期の癖を見る
- 入力: 固定サイズのバイナリチャンク列
- 処理: N スレッドで順にハッシュ化し、最終 checksum を返す
- 条件: スレッド数を 1 / 2 / 4 / 8 のように段階化
単純な tight loop よりも、並列実行時の伸び方 が見えやすいです。
4. startup_noop または startup_parse_small
目的: 起動時間を見る
noop: 起動してすぐ終了parse_small: 小さな入力を 1 回だけ処理して終了
ここでは C# / Java の JIT や初期化コストが見えやすく、C++ / Go の見え方とだいぶ変わります。
逆に言うと、ここで差が出ても、長時間処理の勝敗とは別です。
JSON や HTTP ベンチはどうするか
JSON や HTTP は実務に近いので、もちろん意味はあります。
ただし、その場合は 言語比較というより、ライブラリ・フレームワーク・エコシステム込みの比較 になります。
それ自体は悪くありません。
むしろ実務ではそちらのほうが重要なことも多いです。
ただ、記事やレポートでは
これは言語の比較ではなく、標準的な実装と主要ライブラリ込みの比較です
と明記したほうが誤解が少ないです。
言語ごとに揃えるべき条件
C++
- 最適化ビルドに揃える
- コンパイラを固定する
- 標準ライブラリ実装を固定する
-O3//O2、LTO、PGO などの条件を明記する- 結果が最適化で消えないように注意する
- 未定義動作で速く見えていないかを疑う
C++ は自由度が高いぶん、条件差がそのまま大きく出ます。
そのため、どのコンパイラで、どのフラグで、どの STL で測ったか はかなり重要です。
C#
- Release ビルドに揃える
- .NET のバージョンを固定する
- Server GC / Workstation GC などの条件を記録する
- Tiered Compilation、ReadyToRun、Native AOT の有無を明記する
- cold と warm を分ける
C# は .NET の設定差が見え方を変えます。
特に JIT の C# と Native AOT の C# は、同じ「C#」でも別軸です。
ここを混ぜると、比較対象が言語ではなく 配布形態 になります。
Java
- JDK のベンダーとバージョンを固定する
- GC を明記する
- warm-up / measurement / fork を固定する
- ヒープサイズや JVM オプションを記録する
- cold start と steady-state を分ける
Java は JIT の恩恵を受けやすい反面、初回の見え方はかなり変わります。
そのため、短命プロセスの比較 と 長時間運転の比較 を分けるのが必須です。
Go
- Go のバージョンを固定する
GOMAXPROCSを固定するCGO_ENABLEDを明記するGOGCをいじるなら必ず記録する- 可能なら benchmark 形式の出力を残す
Go は比較的扱いやすいですが、並列ベンチでは GOMAXPROCS の影響が大きいです。
また、cgo を使うかどうかで世界が変わるので、そこは必ず条件に残します。
実行環境の揃え方
どの言語でも、環境を揃えない比較は、だいたい環境を比べています。
揃えるべきもの
- 同じ CPU / メモリ / ストレージ
- 同じ OS バージョン
- 同じ電源条件
- 同じ室温に近い条件
- 同じ入力データ
- 同じプロセス優先度
- 同じコア数条件
- 同じコンテナ or ベアメタル条件
特に効くもの
電源設定と CPU 周波数
ノート PC だと、AC 接続かバッテリーかだけでも別世界になります。
CPU governor や power mode が揃っていないと、比較結果がかなりぶれます。
Windows での電源条件、通知、バックグラウンドノイズ、熱、実行順序の揃え方については、別記事の
Windowsで異なるバージョンのプログラムの実行速度をいかに比較するか
で詳しく整理しています。Windows で測るなら、ここはかなり効きます。
熱
最初の数回だけ速く、後半で落ちるなら、熱やスロットリングを疑います。
A を全部回してから B を全部回すより、A / B / A / B のように交互に回したほうが偏りを減らせます。
バックグラウンド処理
更新、インデックス、同期、ウイルススキャン、ブラウザ、チャットツール。
このへんは地味ですが、普通に刺さります。
何を測るべきか
言語比較では、最低でも次の 4 つを分けて見るのがおすすめです。
1. wall-clock time
ユーザーが待つ実時間です。
まず最初に見るべき指標はこれです。
2. CPU time
「実際に CPU をどれだけ使ったか」です。
wall-clock だけ速くても CPU time が変わらないなら、待ち時間や I/O の影響かもしれません。
3. memory / allocations
- 最大 RSS
- 総割り当て量
- alloc 回数
- GC 回数
- GC pause
このへんを見ると、速さの裏にあるコストが見えます。
4. 分布
- 中央値
- p95 / p99
- min / max
- 標準偏差やばらつき
平均だけで語ると、たまに飛ぶ処理の正体が見えません。
実行手順のおすすめ
実運用しやすい流れをまとめると、だいたい次の順番です。
1. workload を決める
まず、何を比較したいのかを明確にします。
- 起動時間
- 定常 throughput
- tail latency
- メモリ効率
- 並列スケール
2. 共通データセットを固定する
入力データは固定 seed か固定ファイルで揃えます。
データ生成まで含めてしまうなら、それも各言語で同じ条件にする必要があります。
3. 正しさ確認を先に通す
小さいデータと大きいデータで、全実装が同じ結果を返すことを確認します。
checksum やハッシュを出させると扱いやすいです。
4. build 条件を固定する
各言語で Release / 最適化済みの実行形式を作り、バージョンとフラグを記録します。
5. cold と warm を分ける
特に C# と Java はここが重要です。
- cold: プロセス起動直後を含む
- warm: 数回実行後の安定状態
この 2 つは同じ表に混ぜないほうがきれいです。
6. 実行順序を交互またはランダム化する
例:
cpp -> csharp -> java -> go
go -> java -> cpp -> csharp
csharp -> go -> java -> cpp
...
こうすると熱やノイズの偏りが減ります。
7. 回数を確保する
軽い microbenchmark ならかなり多めに、end-to-end なら少なくとも 10 回以上は欲しいです。
差が小さいのに回数が少ないと、解釈がかなり危うくなります。
8. raw data を保存する
集計結果だけでなく、各 run の生データ を残します。
あとで見ると、外れ値や warm-up の癖が読めます。
9. 差が出たら profile を取る
差が出たときは、そこで初めて原因を掘ります。
- CPU profile
- allocation profile
- GC ログ
- flame graph
- OS 側のトレース
ここまで行くと、「速い / 遅い」ではなく、なぜそうなるか が話せるようになります。
結果の読み方
数字が出た後も、読み方を間違えるとやっぱり危ないです。
初回だけ C# / Java が遅い
JIT、クラスロード、初期化の影響を疑います。
この場合、
- 起動時間が重要なら意味のある差
- 長時間運転が主題なら別表に分けるべき差
です。
C++ が tight loop で強い
低レベル最適化、オブジェクト配置、最小限のランタイムオーバーヘッドが効いている可能性があります。
ただし、そこだけを見て「だから実サービスでも最速」と言うのは飛躍です。
Go が起動時間や配布しやすさで有利に見える
単一バイナリ、比較的軽い立ち上がり、扱いやすい並列モデルが効くことがあります。
ただし、すべての CPU 系 workload で有利とは限りません。
C# / Java が steady-state でかなり追いつく、あるいは逆転する
JIT の最適化が効いている可能性があります。
これも珍しい話ではありません。
そのため、起動込みの比較 と 定常状態の比較 を混ぜないことが大事です。
allocation-heavy な処理で差が大きい
この場合は、言語名よりも
- メモリレイアウト
- 文字列や map の扱い
- GC の挙動
- 余分なコピー
のほうが効いていることが多いです。
記録テンプレート
ベンチ結果には、最低でも次の項目を残しておくと後で助かります。
timestamp,language,scenario,run_kind,cold_or_warm,elapsed_ms,cpu_ms,max_rss_mb,alloc_bytes,gc_count,checksum
compiler_or_runtime,compiler_version,flags,os,cpu,threads,input_id,notes
たとえば run_kind はこんな感じで分けられます。
micromacrostartupparallel
cold_or_warm は、少なくとも次のどちらかを明示したいです。
coldwarm
ベンチは、測ること より 後から解釈できること のほうが大事だったりします。
まとめ
C# / C++ / Java / Go の速度比較で本当に大事なのは、
どの言語が最速か という雑な問いを、
どの workload を、どの条件で、どの指標で比較するのか
という実験の形に落とすことです。
特に外しにくいポイントをもう一度まとめると、次の通りです。
- 起動時間と定常状態を分ける
- 同じアルゴリズム、同じ入力、同じ正しさ確認で測る
- 1 本のベンチだけで結論を出さない
- 言語内の benchmark と言語横断の benchmark を分ける
- 平均より中央値と分布を見る
- 条件と raw data を残す
そして最後にいちばん大事なのは、言語名で勝敗を決めようとしすぎないこと です。
現実の性能は、言語、ランタイム、ライブラリ、ビルド条件、データ、OS、ハードウェアの合わせ技で決まります。
「C++ が速い」「Java が強い」「Go が軽い」「C# でも十分速い」という話は、全部ある意味では正しいです。
ただし、どの条件でそう言っているのか が抜けると、だいたい霧の中で殴り合う話になります。
条件を揃えて、複数の workload で、cold / warm を分けて、分布まで見る。
地味ですが、結局これがいちばん強いです。
参考資料
-
BenchmarkDotNet Getting Started
https://benchmarkdotnet.org/articles/guides/getting-started.html -
OpenJDK JMH Project
https://openjdk.org/projects/code-tools/jmh/ -
JMH GitHub Repository / README
https://github.com/openjdk/jmh -
Go
testingpackage
https://pkg.go.dev/testing -
Go
benchstat
https://pkg.go.dev/golang.org/x/perf/cmd/benchstat -
Google Benchmark User Guide
https://google.github.io/benchmark/user_guide.html -
Windows で異なるバージョンのプログラムの実行速度をいかに比較するか
https://comcomponent.com/blog/2026/03/16/002-windows-benchmark-comparing-program-versions/
関連トピック
この記事とあわせて見ると理解しやすいページです。
このテーマの相談先
性能比較の設計、計測条件の揃え方、結果の解釈、原因の掘り下げは、次のサービスと相性がよいテーマです。
関連トピック
このテーマと近いトピックページです。記事を起点に、関連するサービスや他の記事へ進めます。
Windows技術トピック
Windows 開発、不具合調査、既存資産活用の技術トピックをまとめた入口です。
このテーマがつながるサービス
この記事は次のサービスページにつながります。近い入口からご覧ください。
技術相談・設計レビュー
性能比較の設計、計測条件の揃え方、warm-up や統計の読み方まで含めて、技術相談・設計レビューと相性がよいテーマです。
不具合調査・原因解析
言語やバージョンをまたいだ性能差の原因切り分け、ボトルネックの特定、測定手順の妥当性確認は、不具合調査・原因解析として進めやすいです。