Subterranean Flower

JavaScriptで任意の処理にかかる時間を計測する

Author
古都こと
ふよんとえんよぷよぐやま!!!

アプリケーションにおけるパフォーマンスの重要性は、昔も今も変わりません。優れた体験を実現するため、多くの技術者が日夜汗を流しています。

パフォーマンスの改善には計測が必須です。「推測するな、計測せよ」という言葉の通り、実際に計測してみないことには、何もわかりません。

JavaScriptにも、パフォーマンスを計測するためのいくつかの方法が存在します。今回はそれらを紹介してみたいと思います。

さまざまなパフォーマンス計測方法

Date.now()を使った方法(非推奨)

最初に紹介するこの方法は、レガシーで、とてもお勧めできないものです。ですが、一種のアンチパターンとして、ここに掲載しておきます。

Date.now()はシステムにおける現在の日時を、ミリ秒で返すメソッドです。処理開始前の時間と、処理終了後の時間を取得し、引き算することで、処理にかかった時間を計測することができます。

const startTime = Date.now(); // 開始時間
yourFunction(); // 計測する処理
const endTime = Date.now(); // 終了時間

console.log(endTime - startTime); // 何ミリ秒かかったかを表示する

これで問題なく動くように思えます。しかし一体何が問題なのでしょう?

一番大きな問題は、精度が1ミリ秒しかないところです。現在のコンピュータやJavaScript実行エンジンというものは、極めて高速です。場合にもよりますが、1ミリ秒では不十分なことも多いです。

また、Date.now()はシステムの時計に依存します。計測中に時計のズレが補正されたりすると、計測値もズレる可能性があります。値として、あまり信頼できないということです。

performance.now()を使った方法

performance.now()はナビゲーション開始からの経過時間をミリ秒で返すメソッドです。得られたミリ秒は、マイクロ秒(1/1000ミリ秒)の精度を持ちます。つまり、「1ミリ秒」だけではなく、「1.5ミリ秒」や「1.25ミリ秒」といった細かい精度での値が得られます。ただし実際には、ブラウザによる実装は、セキュリティの都合で5マイクロ秒程度の精度となっています。

使い方はDate.now()と同じです。処理開始前と、処理開始後でそれぞれ時間を取得し、引き算するだけです。

const startTime = performance.now(); // 開始時間
yourFunction(); // 計測する処理
const endTime = performance.now(); // 終了時間

console.log(endTime - startTime); // 何ミリ秒かかったかを表示する

Date.now()と違うのは、精度が5マイクロ秒であるところです。Date.now()がミリ秒の精度でしたので、200倍の精度を持つことになります。これでしたら、十分な精度が確保できていると言えます。

また、performance.now()は、ナビゲーション開始からの時間を表すので、システムの時計に依存しません。つまり、「計測値ズレ」の問題は起こらないということです。

簡単な計測においては、performance.now()を使った方法は、最も基本的な手法になるでしょう。

performance.mark()とperformance.measure()を使った方法

performance.now()を使った計測方法は、簡単に素晴らしい結果をもたらしてくれます。しかし、いささかシンプルすぎる気もします。小規模コードに対してのパフォーマンス計測ではおおいに役に立つでしょうが、大きなシステムでは、より柔軟な計測方法が必要になることがあるでしょう。

performance.mark()とperformance.measure()は、まさにそういった機能を提供してくれます。これは、performance.mark()であらかじめ開始点と終了点をマークし、performance.measure()で計測して、後から結果を取り出すことができるメソッドです。

それぞれの計測点には任意の名前をつけることができます。これにより複数の計測点を作ることができます。

performance.mark('myPerformanceStart') // 開始点
yourFunction(); // 計測する処理
performance.mark('myPerformanceEnd') // 終了点

performance.measure(
    'myPerformance', // 計測名
    'myPerformanceStart', // 計測開始点
    'myPerformanceEnd' // 計測終了点
);

// 結果の取得
const results = performance.getEntriesByName('myPerformance');

// 表示
console.log(results[0]);

performance.mark()は計測点を設置します。それぞれに任意の名前をつけることが可能です。これで計測開始点と終了点を打ちます。次にparformance.measure()で計測します。performance.measure()には、「任意の計測名」「開始点の名前」「終了点の名前」を指定します。ここで指定した計測名は、あとで結果を取り出すときに使います。

そしてperformance.getEntries()か、performance.getEntriesByName()、あるいはgetEntriesByType()のいずれかで結果を取り出します。今回は計測名で取り出したいので、performance.getEntriesByName()を使用しました。

これで計測結果が得られます。実際にやっていることはperformance.now()と大して変わりませんが、より柔軟な取り扱いができるAPIとなっています。

ブラウザの開発者ツール

なにもJavaScriptからの計測にこだわる必要はありません。近年のブラウザには優秀なパフォーマンスモニタが搭載されており、それらを利用するという手もあります。

たとえばChromeの開発者ツールにはPerformanceタブが存在し、関数などの実行時間を見ることができます。

使い方は簡単で、Chromeの開発者ツールを開き、Performanceタブを開きます。左上に存在する●ボタンか、その隣にあるリロードボタンを押すと、パフォーマンスの記録が開始されます。

コードには変更を加えず、ブラウザの機能だけで計測できるので、お手軽に実行速度を調べることができます。

実際に計測してみる

機能の説明だけではちょっと物足りない気がしたので、実際に何か測ってみましょう。今回は単純なものしか取り扱わないので、performance.now()を使用します。

毎回、計測処理を書いていると面倒なので、以下のような関数を作ると便利です:

// 関数の実行時間を計測する関数
// 実行にかかった時間をミリ秒で出力
function measure(name, func) {
    const start = performance.now();
    func();
    const end = performance.now();
    
    const elapsed = (end - start);
    const elapsedStr = elapsed.toPrecision(3);
    console.log(`${name}: ${elapsedStr}`);
}

それではさっそくやってみましょう。

今回題材にするのは配列のソート(並び替え)処理です。バブルソートとクイックソートを比べてみましょう。

バブルソートは単純に大きいものと小さいものを入れ替えるだけのアルゴリズムです。単純なので実装しやすいですが、あまり速くはありません。

クイックソートは、ピボットと呼ばれる代表値を選出して、ピボットより小さい値の組と、ピボットより大きい値の組に分け、それぞれの組に対して再び「クイックソート」をします。いわゆる再帰というやつですね。クイックソートは一般に高速なソートを可能にしますが、理論上の最悪計算時間はバブルソートと変わりません。

これら2つのアルゴリズムを実装して計算時間を比較してみましょう。

// 関数の実行時間を計測する関数
// 実行にかかった時間をミリ秒で出力
function measure(name, func) {
    const start = performance.now();
    func();
    const end = performance.now();
    
    const elapsed = (end - start);
    const elapsedStr = elapsed.toPrecision(3);
    console.log(`${name}: ${elapsedStr}`);
}

// テスト用の配列データをランダムに生成する関数
function generateTestData(length) {
    const data = [];
    for(let i=0; i<length; i++) {
        data.push(Math.random());
    }
    return data;
}

// バブルソート
function bubblesort(array) {
    const length = array.length;
    for(let i=0; i<length; i++) {
        for(let j=length-1; j>i; j--) {
            if(array[j] < array[j-1]) {
                [array[j], array[j-1]] = [array[j-1], array[j]];
            }
        }
    }
}

// クイックソート
function quicksortImpl(array, left, right) {
    if(left > right) { return; }
    
    const pivot = array[Math.floor((left+right)/2)];
    let i = left;
    let j = right;
    
    while(true) {
        while(array[i] < pivot) { i++ }
        while(array[j] > pivot) { j-- }
        if(i >= j) { break; }
        
        [array[i], array[j]] = [array[j], array[i]];
        
        i++;
        j--;
    }
    
    quicksortImpl(array, left, i-1);
    quicksortImpl(array, j+1, right);
}

function quicksort(array) {
    quicksortImpl(array, 0, array.length-1);
}

// テストデータの生成
const testData = generateTestData(1000);

// バブルソート計測
measure('bubblesort', () => {
    const data = [...testData]; // コピー
    bubblesort(data);
});

// クイックソート計測
measure('quicksort', () => {
    const data = [...testData];
    quicksort(data);
});

実装できたので実行してみます。

バブルソートが20.3ミリ秒で、クイックソートが2.18ミリ秒です。クイックソート、だいぶ速いですね。(※最悪計算時間はどちらのアルゴリズムも同じなので、使用するデータによってはこのような結果にならない可能性もあります。)

このようにして実行時間を計測することで、どこに時間がかかっているのか、どういうアルゴリズムが適切なのかを知ることができます。処理の重さに困ったら、いろいろ測ってみましょう。