JavaScript
 Computer >> コンピューター >  >> プログラミング >> JavaScript

JavaScriptにおけるループ版と再帰版の階乗関数のパフォーマンス徹底比較

本記事では、数値を引数として受け取り、その階乗を返す2つのJavaScript関数を作成し、それぞれの処理速度を実際に計測して比較します。

1つ目の関数はforループ(またはwhileループ)を使って階乗を計算する反復的なアプローチ、2つ目の関数は自分自身を呼び出す再帰的なアプローチを採用します。最後に、大量のイテレーションを実行した際にかかる時間をconsole.time()メソッドで測定し、両者の性能差を確認します。

コード例

以下が実際のコードです。

const factorial = (num = 1) => {
    let result = 1;
    for (let i = 2; i <= num; i += 1) {
        result *= i;
    }
    return result;
}

const factorialRecursive = (num = 1) => {
    if(num > 1){
        return num * factorialRecursive(num - 1);
    }else{
        return 1;
    }
};

const ITERATIONS = 100000000;
const num = 12;

console.time('Looping Approach');
for(let i = 0; i < ITERATIONS; i++){
    factorial(num);
};
console.timeEnd('Looping Approach');

console.time('Recursive Approach');
for(let j = 0; j < ITERATIONS; j++){
    factorialRecursive(num);
};
console.timeEnd('Recursive Approach');

このコードでは、1億回(100,000,000回)のイテレーションを設定し、引数として12を使用しています。12!は653,837,184,000となり、JavaScriptのNumber型で安全に表現できる範囲に収まる最大の階乗値であるため、精度の問題が発生しない値を選んでいます。

出力結果

コンソールに出力される結果は以下の通りです。

Looping Approach: 886.720ms
Recursive Approach: 6526.203ms

結果の考察:なぜ再帰は遅いのか?

この計測結果から、ループによるアプローチの方が再帰よりも大幅に高速であることがわかります。具体的には、この例ではループ版が約887ミリ秒に対し、再帰版は約6,526ミリ秒と、およそ7倍以上の差が生じています。

この差が生まれる主な理由は以下の通りです。

  • 関数呼び出しのオーバーヘッド: 再帰では各ステップごとに新しい関数呼び出しが発生するため、スタックフレームの作成・破棄のコストが積み重なります。
  • コールスタックへの負荷: 関数を呼び出すたびにコールスタックに情報が積まれるため、メモリ管理の負担が増大します。深い再帰ではスタックオーバーフローのリスクもあります。
  • JITコンパイルの最適化: モダンなJavaScriptエンジンは単純なループ構造を高度に最適化できますが、再帰呼び出しはその恩恵を受けにくい傾向があります。

ただし、絶対的な実行時間はマシンのスペックやブラウザ・Node.jsのバージョンによって変動しますが、「ループ版が再帰版より高速である」という比率自体はほぼ一定に保たれます。

まとめ:どちらを使うべきか?

パフォーマンスが重要な場面ではループによる実装が有利ですが、再帰には「数学的な定義とコードが一致し可読性が高い」という利点もあります。また、ES2015以降の末尾呼び出し最適化(TCO)をサポートする環境では、末尾再帰として書き直すことでスタック消費を抑えることも可能です。用途や規模に応じて、適切な手法を選択することが重要です。

  1. JavaScriptにおける関数とメソッドの違いとは?わかりやすく解説

    JavaScriptにおいて、関数とメソッドは本質的には同じものです。両者の違いは、メソッドがオブジェクトのプロパティとして定義された関数であるという点にあります。つまり、独立して定義されたものを「関数」、オブジェクトに紐づけられたものを「メソッド」と呼びます。 JavaScriptの関数の基本形 まず、一般的な関数の定義方法を見てみましょう。 function functionname(param1, param2){ // 処理内容 } JavaScriptのメソッドの例 メソッドは、オブジェクトに関連付けられた関数です。次の例では、employeeオブジェクトの中にdetails

  2. JavaScriptの分割代入を使った関数パラメータの受け渡し方法

    JavaScriptでは、関数にオブジェクトを引数として渡す際、「分割代入(デストラクチャリング)」と呼ばれる構文を利用することで、オブジェクトの各プロパティを個別の変数として受け取ることができます。 さらに、各プロパティにデフォルト値を指定しておけば、対応するプロパティが存在しない場合でもエラーにならず、あらかじめ設定した値が使用されます。これにより、安全で柔軟な関数設計が可能になります。 サンプルコード 以下は、分割代入を使って関数のパラメータを受け取るJavaScriptのコード例です。 <!DOCTYPE html> <html lang="en&quo