広告

Java : 経過時間の測定には System.nanoTime を使おう

例えば、プログラムの処理時間を測定するときに System.currentTimeMillis を使ってはいないでしょうか?
本記事では、経過時間の測定には System.nanoTime を使うことをおすすめしつつ、その理由について解説します。


概要

プログラムのパフォーマンスを調べるために、要所要所で処理時間を測定したいこともあるかと思います。
例えば、次のような main メソッドがあったとして、func1 と func2 の処理時間を測定するにはどうしたらよいでしょうか。

void main() {
    func1();
    func2();
}

1つの方法としては、func1 と func2 メソッドの呼び出し前後で時間を測定することです。
そんなときに使える API として、System.currentTimeMillis を思い浮かべたかたもいらっしゃるかもしれません。

System.currentTimeMillis は現在時刻をミリ秒で返します。

void main() {
    final var time1 = System.currentTimeMillis();
    func1();
    final var time2 = System.currentTimeMillis();
    func2();
    final var time3 = System.currentTimeMillis();

    System.out.println("func1 time : " + (time2 - time1));
    System.out.println("func2 time : " + (time3 - time2));
}

結果は次のようになりました。

func1 time : 24
func2 time : 201

func1 は 24ミリ秒、func2 は 201ミリ秒の処理時間でした。
System.currentTimeMillis でも処理時間を測定できていますね。

しかし、currentTimeMillis より、もっと経過時間の測定に適した API があります。
それが System.nanoTime です。

currentTimeMillis と nanoTime の違い

System クラスには、時間に関するメソッドが2つあります。
currentTimeMillisnanoTime メソッドです。

名前からは分かりづらいのですが、この2つは用途がまったく違います。
簡単に2つの違いを見ていきましょう。


public static long currentTimeMillis()
ミリ秒で表される現在の時間を返します。

System.currentTimeMillis現在時刻 を返します。

// 現在時刻をミリ秒で取得。
final var time = System.currentTimeMillis();
System.out.println(time); // 1679209828914

// 日時として表示。
final var instant = Instant.ofEpochMilli(time);
System.out.println(instant); // 2023-03-19T07:37:44.283Z

現在時刻の取得には、他にも Instant.nowLocalDateTime.now など、代わりとなる API がいろいろとあります。


public static long nanoTime()
実行中のJava仮想マシンの高精度時間ソースの現在値を、ナノ秒の単位で返します。このメソッドは、経過時間を測定するためだけに使用できます。

System.nanoTime は経過時間の測定のみに使います。
API仕様にも、はっきりと「経過時間を測定するためだけに使用できます」と記述されていますね。

nanoTime が返す値は、現在時刻とは関係ありません。
よって日時には変換できません。

final var startTime = System.nanoTime();
System.out.println(startTime); // 18847675664600

// 1秒スリープ
TimeUnit.SECONDS.sleep(1);

final var endTime = System.nanoTime();
System.out.println(endTime); // 18848688620500

final var elapsedTime = endTime - startTime;
System.out.println(elapsedTime); // 1012955900

// 秒に変換
System.out.println(elapsedTime / 1000000000.0 + " sec."); // 1.0129559 sec.

上記の例では、1秒のスリープの前後で nanoTime を使い経過時間を測定しています。
nanoTime の単位はナノ秒です。最後に 1,000,000,000 で割って秒に変換しています。

それでは nanoTime の利点についても見ていきましょう。


nanoTime の利点

高精度

public static long nanoTime()
...
このメソッドが提供する精度はナノ秒ですが、その解像度(つまり値の変更頻度)は必ずしもナノ秒ではありません。

System.nanoTime はナノ秒単位で現在値を返します。一方、System.currentTimeMillis はミリ秒単位です。
nanoTime のほうが 1,000,000倍精度が高いことになります。(ナノ秒 → マイクロ秒 → ミリ秒)

ただし、ドキュメントにもあるように、どの環境でもナノ秒の精度(解像度)で測定できるわけではありません。

自分の環境 (Windows 10) では、下2桁が必ず0となりました。
よって、精度は100ナノ秒のようです。

final var startTime = System.nanoTime();
System.out.println(startTime); // 18847675664600

// 1秒スリープ
TimeUnit.SECONDS.sleep(1);

final var endTime = System.nanoTime();
System.out.println(endTime); // 18848688620500

final var elapsedTime = endTime - startTime;
System.out.println(elapsedTime); // 1012955900

// 秒に変換
System.out.println(elapsedTime / 1000000000.0 + " sec."); // 1.0129559 sec.

値が巻き戻ることがない

public static long nanoTime()
...
システムのほかの概念や壁時計の時刻に関連していません。

System.currentTimeMillis は現在の時刻を返します。
つまり、マシンの時計に依存しています。

一方、System.nanoTime は時計に依存していません。
いくらマシンの時刻をいじろうとも影響はしません。

実際にコード例で確認してみましょう。
10秒スリープする前後で、currentTimeMillisnanoTime それぞれを使って経過時間を測定します。

final var startTime1 = System.currentTimeMillis();
final var startTime2 = System.nanoTime();

// 10秒スリープ
TimeUnit.SECONDS.sleep(10);

final var endTime1 = System.currentTimeMillis();
final var endTime2 = System.nanoTime();

final var elapsedTime1 = endTime1 - startTime1;
final var elapsedTime2 = endTime2 - startTime2;

// 秒に変換
System.out.println(elapsedTime1 / 1000.0 + " sec.");
System.out.println(elapsedTime2 / 1000000000.0 + " sec.");

まずはマシンの時刻はいじらずにそのまま実行します。

10.004 sec.
10.0029617 sec.
  • 1行目 : currentTimeMillis を使った測定結果
  • 2行目 : nanoTime を使った測定結果

どちらも 10秒と+誤差といった感じですね。

次に、10秒のスリープ中にマシンの時刻を 30秒巻き戻します。
今回は Windows の PowerShell で次のコマンドを使いました。

> Set-Date -Adjust -00:00:30

結果は次のようになりました。

-19.251 sec.
10.0021223 sec.

currentTimeMillis のほうは、約-20秒となりました。
経過時間を測定しているのに、結果がマイナスになるのはよくないですね。

nanoTime のほうは約10秒となり期待どおりです。
時刻をいじってもいじらなくても結果は変わりません。

今回は、手動で少し強引に時刻を変更しました。
しかし、NTP(Network Time Protocol) などで時刻の同期を自動で行っているマシンでは、値が巻き戻る可能性は 0 ではありません。

もし currentTimeMillis を使うのであれば、経過時間がマイナスになることも考慮しなくてはなりません。
nanoTime を使えばその考慮は必要ありません。そのためコードはシンプルになるでしょう。

オーバーフローに注意

public static long nanoTime()
...
次は使用しません
if (System.nanoTime() >= startTime + timeoutNanos) ...
数値オーバーフローの可能性があるためです。

引用先の APIドキュメントにもありますが、nanoTime の値を計算に使うときはオーバーフローに注意しましょう。

基本的には、終了地点(endTime)から開始地点(startTime)を引き算しましょう。
足し算してしまうと、longの最大値をオーバーして、意図しない結果になる可能性があります。

まとめ

経過時間の測定には、System.currentTimeMillis ではなく System.nanoTime を使いましょう。
System.nanoTime は高精度で値が巻き戻ることもないので、経過時間の測定にぴったりです。


関連記事

ページの先頭へ