🏎️

言語の速度比較:Rust, Go, Node.js, PHPを検証!

に公開1

こんにちは、Southanです!
プログラマー界の永遠のテーマ「結局、どの言語が速いの?」

……その問いに、今、終止符を打ちにきた!

シャンクスか! とツッコまれそうですが、普段使ってるRust、Go、PHP、Node.jsの4言語で、速度比較をしました。
ただし今回はCPUメインで、大量の計算と関数呼び出しを通じて、各言語がどれだけの速度を発揮できるかを測ってみました。実務ではデータベースやネットワークのI/O待ちが大半ですが、今回は純粋な「計算力」にフォーカスしています!

今回の検証に使用したソースコードはこちらで公開しています。

測定方法:フィボナッチ数列の「素朴な再帰」で深掘り

今回の速度測定には、フィボナッチ数列の「Naive Recursive(素朴な再帰)」という方法を採用しました。

フィボナッチ数列とは?


フィボナッチ数列とは、最初の2項(0と1)に始まり、それ以降の各項が直前の2つの項の和となる数列です。

0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55...

Naive Recursive(素朴な再帰)とは?


フィボナッチ数列の数学的定義をそのままコードに落とし込んだ実装が、「Naive Recursive(素朴な再帰)」です。以下にRustの例を示します。

fn fib(n: u64) -> u64 {
    match n {
        0 => 0,
        1 => 1,
        _ => fib(n - 1) + fib(n - 2), // 自分自身を再帰呼び出し
    }
}

なぜNaive Recursive(素朴な再帰)を選んだのか?


  • 呼び出し&スタック性能が明確に現れる
    • fib(44)のような大きな数では、この関数は20億回以上も呼び出されます。関数が呼び出されるたびに、「戻り先」や「ローカル変数」がスタックフレームに積まれ、戻るときにはそれが解放されます。このスタックの扱い方は、言語によって大きく異なります。

      • Rust: OSのネイティブスタックに直接小さなフレームを積むため、高速です。
      • Go: 必要に応じてスタックを伸縮させますが、コピーが発生する際にはコストがかかります。
      • Node.js / PHP: VMがヒープ上で大きめのフレームを管理するため、オーバーヘッドが大きい傾向があります。
  • 同じコードで簡単にできる
    • Naive Recursive(素朴な再帰)はわずか3〜4行で書け、特別なライブラリや高度な最適化テクニックは不要です。
  • 強み・弱みが見える
    • コンパイル言語(Rust / Go)は、ネイティブコード生成と効率的なスタック操作により高速です。一方、JIT言語(Node.js / PHP)は実行しながら最適化を行うものの、呼び出しが多すぎると最適化が追いつかず、速度が低下する傾向があります。この違いがベンチマーク結果の数字として現れます。

実行環境

今回のベンチマークは、以下の環境で実施しました。

ハードウェア環境


  • マシン: MacBook Pro 14インチ
  • プロセッサ: Apple M3 Pro チップ
  • メモリ: 36 GB
  • OS: macOS 14.5.0 (darwin 24.5.0)

言語・ランタイムバージョン


言語 バージョン
Rust rustc 1.78.0
Go go1.24.3
Node.js v24.3.0
PHP 8.4.10

ベンチマークツール


  • hyperfine: v1.19.0
  • 各言語で10回実行して統計処理
  • CSV形式で結果出力

実行結果

それでは、CSVに出力された実行結果をまとめたものがこちらです。

言語 平均時間 標準偏差 最小値 最大値 10回合計 相対速度
Rust 2.44 秒 0.12 秒 2.39 秒 2.79 秒 24.4 秒 1.0×
Go 5.36 秒 0.16 秒 5.30 秒 5.82 秒 53.6 秒 2.2×
Node.js 6.81 秒 0.02 秒 6.78 秒 6.84 秒 68.1 秒 2.8×
PHP 108.36 秒 9.05 秒 96.02 秒 122.34 秒 1,083.6 秒(約18 分) 44.4×

🏆 Rustの圧勝!

平均2.44秒という圧倒的な結果でした。特に印象的だったのは安定性です。標準偏差はわずか0.12秒で、最小値2.39秒から最大値2.79秒まで、ほぼ一定の性能を維持しています。これは、rustc -Oによる最適化と、ネイティブバイナリの力が表れた結果と言えそうです。

🥈 Goも大健闘!

GoはRustの約2.2倍の速度でしたが、10回合計でも53.6秒と1分以内に収まりました。標準偏差0.16秒も良好で、コンパイル言語としての安定した高い性能を発揮しています。

🥉 Node.jsの意外な安定性

10回合計で68.1秒と、約1分強という結果でした。驚いたのは、Node.jsの標準偏差が0.02秒という安定性です。10回の実行でほぼ同一の最適化を実現し、6.78〜6.84秒という非常に狭い範囲に収束しました。

💀 PHPは苦戦…

平均108.36秒、つまり1分48秒という結果で、Rustと比べて44倍も遅いという結果となりました。
さらに、標準偏差が9.05秒と不安定さも目立ち、最小96秒から最大122秒まで、26秒もの幅がありました。実際にベンチマークを実行中、PHPだけが異常に遅く、「PCがフリーズしたのでは?」と心配になるほどでした。トホホ

総評:性能差の背景と実務での見方

今回のアルゴリズムは20億回近く関数を呼び出すため、非常に極端なケースでの比較となりましたが、各言語の純粋な性能特性を比較できたのではないかなと思います。

しかし、実際のアプリケーション開発において、これほど関数呼び出しが繰り返される場面は稀です。実サービスでは、I/O(DBアクセスや外部APIとの通信)がボトルネックになることが多く、その際の言語間の速度差は今回の結果よりもずっと小さくなります。

今回のベンチマークは、各言語の「性能」をミクロな視点で深掘りした結果です。この結果をもって「この言語が良い・悪い」という話ではないことをご理解いただければ幸いです。

言語を選択する際には、速度だけでなく、以下の要素も総合的に考慮することが重要だと考えます。

  • 習得コスト
    • 新しい言語を学ぶ際の難易度や学習曲線の緩やかさ。
  • エコシステムの豊富さ
    • 利用できるライブラリ、フレームワーク、ツールの充実度。
  • 開発チームのスキルセット
    • チームメンバーが既に得意とする言語か。

これらの要素を総合的に考慮し、プロジェクトの特性や要件に最適な言語を選択していきたいですね!
最後までお読みいただき、ありがとうございました!

Discussion

NoboNoboNoboNobo

Goは意図的に設計上再帰呼び出しに関する最適化を行っていません。
一般的なGopherは深い再帰に関する実装はループに置き換えます。
例えばこのfib関数は以下のように書きます。

func fib(n uint64) uint64 {
    if n == 0 {
        return 0
    }
    var a, b uint64 = 0, 1
    for i := uint64(1); i < n; i++ {
        a, b = b, a+b
    }
    return b
}

この記述に変えた場合、計算時間はおそらく百倍くらい早くなります。お試しください。