java-recipes

ホーム パフォーマンス計測 › Perf-01

Perf-01: 処理時間測定(System.nanoTime)

Java で処理時間を正確に計測する方法を解説します。System.nanoTime() System.currentTimeMillis() の違い、 JIT コンパイルの影響を受けないウォームアップ計測、 文字列結合 vs StringBuilder・ArrayList vs LinkedList の速度比較を学びましょう。

いつ使うか

  • 処理が遅いと感じた箇所でボトルネックを特定したいとき
  • アルゴリズムや実装方法を変えたときに改善効果を数値で確認したいとき
  • バッチ処理の全体所要時間をログに記録したいとき
  • ループ回数やデータ量を変えたときの処理時間の変化を測りたいとき

nanoTime vs currentTimeMillis の比較

メソッド精度用途注意点
nanoTime()ナノ秒処理時間計測(相対)JVM 内の相対値のみ有効。壁時計時間ではない
currentTimeMillis()ミリ秒現在時刻の取得・記録OS の時刻同期の影響を受ける。短い処理計測には不向き

サンプルコード

PerformanceSample.java
public class PerformanceSample {

    // System.nanoTime() による処理時間計測
    // nanoTime: 相対時間計測に使う(OS の時刻同期の影響を受けない)
    // currentTimeMillis: 壁時計時間なので OS の時刻調整の影響を受ける
    public static long measureNanoTime(Runnable task) {
        long start = System.nanoTime();
        task.run();
        long end = System.nanoTime();
        return end - start;
    }

    // ループ処理の計測例: 文字列結合 vs StringBuilder
    public static void compareStringConcatenation(int n) {
        // 方法1: 文字列結合(+ 演算子)
        long time1 = measureNanoTime(new Runnable() {
            @Override
            public void run() {
                String result = "";
                for (int i = 0; i < n; i++) {
                    result = result + i; // 毎回新しい String オブジェクトを生成
                }
            }
        });

        // 方法2: StringBuilder
        long time2 = measureNanoTime(new Runnable() {
            @Override
            public void run() {
                StringBuilder sb = new StringBuilder();
                for (int i = 0; i < n; i++) {
                    sb.append(i); // 内部バッファを使い回す
                }
                sb.toString();
            }
        });

        System.out.printf("文字列結合(%d回): %,d ms%n", n, time1 / 1_000_000);
        System.out.printf("StringBuilder(%d回): %,d ms%n", n, time2 / 1_000_000);
        if (time2 > 0) {
            System.out.printf("StringBuilder は約 %.1f 倍高速%n", (double) time1 / time2);
        }
    }

    // ウォームアップ付き計測(JIT コンパイルの影響を除く)
    public static long measureWithWarmup(Runnable task, int warmupCount, int measureCount) {
        // ウォームアップ実行(JIT コンパイルを促す)
        for (int i = 0; i < warmupCount; i++) {
            task.run();
        }

        // 計測実行(複数回の平均)
        long total = 0;
        for (int i = 0; i < measureCount; i++) {
            total += measureNanoTime(task);
        }
        return total / measureCount;
    }

    // ArrayList vs LinkedList のランダムアクセス比較
    public static void compareListAccess(int size) {
        java.util.List<Integer> arrayList = new java.util.ArrayList<>();
        java.util.List<Integer> linkedList = new java.util.LinkedList<>();

        for (int i = 0; i < size; i++) {
            arrayList.add(i);
            linkedList.add(i);
        }

        long t1 = measureNanoTime(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 1000; i++) {
                    arrayList.get(size / 2);
                }
            }
        });

        long t2 = measureNanoTime(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 1000; i++) {
                    linkedList.get(size / 2); // O(n) でリストを辿る
                }
            }
        });

        System.out.printf("ArrayList.get(中間) 1000回: %,d ns%n", t1 / 1000);
        System.out.printf("LinkedList.get(中間) 1000回: %,d ns%n", t2 / 1000);
    }

    public static void main(String[] args) {
        System.out.println("=== 文字列結合 vs StringBuilder ===");
        compareStringConcatenation(10000);

        System.out.println("\n=== ArrayList vs LinkedList ランダムアクセス ===");
        compareListAccess(10000);

        System.out.println("\n=== ウォームアップ付き計測例 ===");
        Runnable task = new Runnable() {
            @Override
            public void run() {
                long sum = 0;
                for (int i = 0; i < 100000; i++) {
                    sum += i;
                }
            }
        };
        long avgNs = measureWithWarmup(task, 5, 10);
        System.out.printf("100000回ループの平均: %,d ns (%.3f ms)%n", avgNs, avgNs / 1_000_000.0);
    }
}

Java 8 では Runnable を匿名クラスで記述します。System.nanoTime() の戻り値はナノ秒(10億分の1秒)なので、1_000_000 で割るとミリ秒になります。

よくあるミス・注意点

nanoTime() の差は同一 JVM 内の比較にのみ使う

System.nanoTime() の戻り値は、 特定の基準点からの経過ナノ秒数です。この基準点は JVM ごとに異なるため、 異なる JVM やマシン間で値を比較することはできません。 同一プロセス内で「開始時刻 - 終了時刻」として差を計算する用途にのみ使用してください。

最初の数回はウォームアップとして計測から除外する

Java では JIT(Just-In-Time)コンパイラがプログラム実行中にコードを最適化します。 最初の数回の実行はまだ JIT 最適化が行われていないため、実際より遅い結果になります。 正確なベンチマークを行うには、計測前に同じ処理を数回「ウォームアップ」として実行しましょう。

マイクロベンチマークには JMH を使う

数マイクロ秒(μs)オーダーの細かい処理を計測する場合、JIT コンパイラ・GC(ガベージコレクション)・ OS のスケジューラが測定結果に大きく影響します。 本格的なベンチマークには JMH(Java Microbenchmark Harness)という専用ツールを使うことが推奨されています。

currentTimeMillis() は短い処理の計測に使えない

System.currentTimeMillis() はミリ秒精度ですが、 OS によっては 10〜15 ms 単位でしか値が更新されません。 1 ms 未満の処理を計測すると常に 0 ms になります。短い処理の計測には必ず nanoTime() を使いましょう。

テストする観点

  • measureNanoTime が正の値(0 より大きい値)を返すこと
  • StringBuilder 版の処理時間が文字列結合(+ 演算子)より短いこと(大きな n で有意な差が出る)
  • ArrayList.get() の処理時間が LinkedList.get() より短いこと(ランダムアクセス比較)
  • ウォームアップあり計測の結果がウォームアップなしより安定すること(標準偏差が小さい)
  • warmupCount=0 や measureCount=1 などの境界値でも例外が発生しないこと

GitHub でソースコードを見る →