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つあります。
currentTimeMillis と nanoTime メソッドです。
名前からは分かりづらいのですが、この2つは用途がまったく違います。
簡単に2つの違いを見ていきましょう。
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.now や LocalDateTime.now など、代わりとなる API がいろいろとあります。
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 の利点
高精度
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.
値が巻き戻ることがない
System.currentTimeMillis は現在の時刻を返します。
つまり、マシンの時計に依存しています。
一方、System.nanoTime は時計に依存していません。
いくらマシンの時刻をいじろうとも影響はしません。
実際にコード例で確認してみましょう。
10秒スリープする前後で、currentTimeMillis と nanoTime それぞれを使って経過時間を測定します。
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 を使えばその考慮は必要ありません。そのためコードはシンプルになるでしょう。
オーバーフローに注意
引用先の APIドキュメントにもありますが、nanoTime の値を計算に使うときはオーバーフローに注意しましょう。
基本的には、終了地点(endTime)から開始地点(startTime)を引き算しましょう。
足し算してしまうと、longの最大値をオーバーして、意図しない結果になる可能性があります。
まとめ
経過時間の測定には、System.currentTimeMillis ではなく System.nanoTime を使いましょう。
System.nanoTime は高精度で値が巻き戻ることもないので、経過時間の測定にぴったりです。