torutkのブログ

ソフトウェア・エンジニアのブログ

高精度タイマーQueryPerformanceCounterの分解能

JavaのSystem.nanoTime()メソッドは、高分解能のタイマーからその現在値をナノ秒単位の時間で取得するネイティブメソッドです。

高分解能タイマーにはいくつか種類がありますが(後述)、その仕組みは、ハードウェア的にはある周波数で動作するクロックの回数を数えるカウンターとなっています。

ソフトウェアとしては、まずカウンタ値を取得し、次にそのタイマーの周波数を用いてカウンタ値から時刻に変換します。例えば1MHzのクロックのタイマーでカウンタ値が100であれば、

100 / (1 * 1000000) = 0.0001 [sec]

となります。

JavaのSystem.nanoTime()がどのような処理を行っているのか、Oracle JDK(OpenJDK)7のソースコードを調べると、Windows OSの場合はWindows APIのQueryPerformanceCounterを使い、高精度タイマーのカウント値と周波数(分解能:Hz)から時刻(PCが起動してからの積算時間)をナノ秒単位で算出しています。

System.nanoTime()を使用する典型的な状況は、ベンチマーク等である短い処理時間を計測したいときに、その処理の開始前の時刻と処理完了直後の時刻を取得し、処理時間を算出するときです。

このような時間計測にあたっては、分解能(精度)が重要になるので、タイマーの精度(周波数)の情報は欠かせません。

しかし、JavaではSystem.nanoTime()の分解能(周波数)を取得する手段が見当たりません。

そこで、分解能がどれだけあるかをWindows上のC/C++プログラムから、Windows APIのQueryPerformanceFrequencyを呼び出すことで、高精度タイマーの周波数を取得することができます。

QueryPerformanceFrequency で周波数を取得

C/C++コード
#include <Windows.h>
#include <iostream>
#include <clocale>

int main()
{
  std::setlocale(LC_ALL, "Japanese_Jpn.932");

  LARGE_INTEGER frequency;
  BOOL ret = QueryPerformanceFrequency(&frequency);
  if (ret == 0) {
    std::wcerr << L"この計算機は高精度タイマーをサポートしていません。" << std::endl;
    return 1;
  }
  std::wcout << L"この計算機の高精度タイマー分解能は、" <<
      frequency.QuadPart << L" Hzです。" << std::endl;
}

次のスペックのPC上で実行しました。

CPU AMD Phenom II X6 1055T (バス速度200MHz, 倍率14)
チップセット AMD870 + AMD SB850
OS Windows 7 Professional 64bit版 SP1
C:\work> QueryPerformanceFrequencySample.exe
この計算機の高精度タイマー分解能は、2743632 Hzです。

C:\work> 

となります。分解能が2,743,632MHzなので、時間に直すと364.4480ナノ秒になります。

Javaだけで周波数を取得する

JNAライブラリを使うと、C/C++コンパイラを使わずにJavaからWindows APIを呼ぶことができそうです。後日取組予定。

System.nanoTime()の連続実行で分解能と比較

System.nanoTime()を繰り返し呼び出してみました。(ソースコード片を次に示します)

    long[] record = new long[aCount];

    for (int i=0; i<aCount; ++i) {
        record[i] = System.nanoTime();
    }

System.nanoTimeを16回連続実行し、nanoTimeの戻り値を表示します。

  0 : 657,854,142,922,228
  1 : 657,854,142,922,228
  2 : 657,854,142,922,593
  3 : 657,854,142,922,593
  4 : 657,854,142,922,593
  5 : 657,854,142,922,957    <- 364ns 増加
  6 : 657,854,142,922,957
  7 : 657,854,142,922,957
  8 : 657,854,142,923,322    <- 365ns 増加
  9 : 657,854,142,923,322
 10 : 657,854,142,923,322
 11 : 657,854,142,923,322
 12 : 657,854,142,923,686    <- 364ns 増加
 13 : 657,854,142,923,686
 14 : 657,854,142,923,686
 15 : 657,854,142,924,051    <- 365ns 増加

ほぼ分解能(周波数)と同じ刻みで値が増加しています。

System.nanoTime() 自体は約100ns位で実行できているので、思った以上に処理が速い、という印象です。1usくらいかかるとどこかで見た気がするのですが・・・。

高精度タイマーの種類と選択

さきほどは、OSのデフォルト設定で選択される高精度タイマーで分解能(周波数)を取得していました。

QueryPerformancCounterは、PC/AT互換機の持つ幾種類かのタイマーから1つ(または複数の組み合わせ?)を用いてカウンタ値を取得します。

昨今の一般的なPC/AT互換機においてQueryPerformanceCounterが使うであろうタイマーは次があります。

タイマー種類 概要 読み出し方法
PIT 1,193,182Hzで動作するタイマ。CPUの外部にありシングルCPUに(のみ)対応 8bit I/Oポート
ACPI PMT 3,579,545Hzで動作するタイマ。ACPI対応マザーボードチップセット)に搭載 32bit I/Oポート
Local APIC CPUのバスクロックを分周した周波数で動作するタイマ。CPU内の割り込みコントローラに搭載。 メモリマップドI/O、最新はMSR?
HPET 10MHz以上で動作すべきタイマ。チップセット(サウスまたはノースブリッジ)に搭載 メモリマップドI/O
TSC CPUクロックで動作するタイマ。CPUのレジスタとして存在 CPU命令RDTSC/RDTSCP
  • 略語
    • PIT:Programmable Interval Timer
    • ACPI PMT:Advanced Configuration and Power Interface Power Management Timer
    • Local APIC:Local Advanced Programmable Interrupt Controller
    • HPET:High Precision Event Timer
    • TSC:Time Stamp Counter
最初の実測周波数はどのタイマか?

Windows OSでは、QueryPerformanceCounterが使用するタイマのデバイスがどれかを調べる方法が見当たりませんでした。

先ほどのQueryPerformanceFrequencyで実測した周波数は2.7MHzと、HPETやTSCに比べると低い周波数で、またPITやACPI PMTとは周波数値が異なります。そこで推定では、Local APICと思われます*1

HPETに切り替える

Windows 7の起動オプションでHPETを使用する設定を定義できます。

管理者権限でコマンドプロンプトを開き、次を実行します。

  • HPETを使用する
C:\> bcdedit /set useplatformclock true
  • HPETの使用設定を削除する
C:\> bcdedit /deletevalue useplatformclock

OSを再起動すると、bcdeditの設定が反映されます。

useplatformclockを有効にして再起動後、QueryPerformanceFreqency APIを実行してみたところ、分解能が14,318,180MHzとなっていました。これは時間に直すと69.814279ナノ秒になります。

次に、System.nanoTime()を繰り返し呼び出してみました。

  0 : 3,432,378,944,390
  1 : 3,432,378,945,368   <- 978ns 増加
  2 : 3,432,378,946,346   <- 978ns 増加
  3 : 3,432,378,946,835   <- 489ns 増加
  4 : 3,432,378,947,813   <- 978ns 増加
  5 : 3,432,378,948,790   <- 977ns 増加
  6 : 3,432,378,949,768   <- 978ns 増加
  7 : 3,432,378,950,257   <- 489ns 増加
  8 : 3,432,378,951,235   <- 978ns 増加
  9 : 3,432,378,952,213   <- 978ns 増加
 10 : 3,432,378,953,190   <- 977ns 増加
 11 : 3,432,378,954,168   <- 978ns 増加
 12 : 3,432,378,954,657   <- 489ns 増加
 13 : 3,432,378,955,635   <- 978ns 増加
 14 : 3,432,378,956,613   <- 978ns 増加
 15 : 3,432,378,957,590   <- 977ns 増加

分解能は69nsのはずですが、System.nanoTime()の1回の呼び出しに978nsかかっています。

使用する高精度タイマー 分解能 System.nanoTime()処理時間
Local APIC + TSC ? 364 ns 約100ns
HPET 70 ns 約980ns

ハードウェアのタイマーとしては、HPETの方が1桁分解能が高いのですが、タイマーの取得にかかる時間を加味すると、Local APIC
が400nsに対してHPETが約1usと却って劣化してしまいます。

うーん、つくづくタイマーは難しいです。

JDK7 のSystem.nanoTime()のネイティブ実装

OpenJDK 7u6のソースコードを追って調査した内容を次の記事に追記しました。

*1:APIの実行速度がI/Oアクセスを伴うにしては早いので、Local APIC + TSCという可能性もあります。