大抵の計算機は、現在時刻として世界協定時(UTC:Coordinated Universal Time)などの時刻を保持して更新しています。これによって、ユーザが認識している「現在時刻」と互換性のある時刻をプログラム内で使用することができます。ただし、UTC時刻と合わせるための仕組みが必要になります。また、計算機は時刻の進みを次節で説明するクロックから算出して更新しているため、定期的にUTC時刻と同期を行なわないと徐々にずれていくことになります。
UTC時刻は、グリニッジ子午線(経度0度)での真夜中を0時0分0秒として1日の時間を表現します。しかし、ユーザの生活している国・地域(ロケール)では、それぞれロケール毎に基準となる子午線(経度)を決めて時刻を表現しています。例えば日本標準時は東経135度を子午線として時刻を表現します。UTC時刻とは9時間(経度15度につき1時間の差)のずれが生じます。
コンピュータの中では、ユーザの時刻とは別に内部の電子回路が動作するための基準となる電気信号クロック(日本語に訳すと「時計」となってしまうが意味が異なる)を持っています。プログラム内でこのクロックに基づく時刻を使用することができます。
クロックは大抵の場合電子回路上で発振器によって得られる一定の周波数信号に基づいて刻まれます。したがって、同じ1クロックでも計算機のハードウェアによって長さが異なります。例えば、1GHzのCPUを持つ計算機において計測されるクロックは、1ナノ秒の分解能となります。ただし、クロックが基準とする発振器は素材の特性や環境によって周波数にずれやゆらぎが生じるため、クロックで算出する1秒と前述のUTCにおける1秒とでは長さに誤差が生じます。
現在のシステム時刻を、UTC1970年1月1日0時0分0秒からの経過時間でミリ秒単位で取得します。ただし、精度はプログラムを実行しているプラットフォームに依存し、Windows OSのPCでは10ミリ秒や15ミリ秒が典型的な値です。
System.currentTimeMillisメソッドはnativeメソッドです。以下のように実装されています。(1)
プログラム内において、ある瞬間から別の瞬間までの時間を計測するニーズがあります。処理時間の計測などが例です。この場合、システム時刻(カレンダー)は不要です。ただし、プログラムは非常に高速に実行されるため、ある瞬間の時刻を取得する際の分解能が重要となります。
Java 2 Standard Edition, v5.0から追加されたメソッドです。プログラムを実行しているプラットフォーム上で利用可能なもっとも正確(高分解能)なクロックの値を取得します。クロックの値は相対的なもので、例えばシステム起動後のCPUクロックのカウント値などが典型的なものです。APIとしてはナノ秒単位の値を返却しますが、実際に得られる精度はプラットフォームで利用可能なクロックに依存します。
System.nanoTimeメソッドは、nativeメソッドで、以下のように実装されています。(2)
ネイティブコードを追うには、JDKのソースコードを入手します。
まず、System.nanoTime()は、ソースファイルopenjdk/jdk/src/share/classes/java/lang/System.javaにおいて次のように定義されています。
public static native long nanoTime();
これに対応するネイティブのコードはソースファイルopenjdk/jdk/src/share/native/java/lang/System.cにおいて次のように記述されています。
static JNINativeMethod methods[] = { {"currentTimeMillis", "()J", (void *)&JVM_CurrentTimeMillis}, {"nanoTime", "()J", (void *)&JVM_NanoTime}, {"arraycopy", "(" OBJ "I" OBJ "II)V", (void *)&JVM_ArrayCopy}, };
次に、JVM_NanoTime関数の定義されているファイルを検索します。検索結果、ソースファイルopenjdk/hotspot/src/share/vm/prims/jvm.cppにおいて次のように記述されています。
JVM_LEAF(jlong, JVM_NanoTime(JNIEnv *env, jclass ignored)) JVMWrapper("JVM_NanoTime"); return os::javaTimeNanos(); JVM_END
次に、javaTimeNanosメンバ関数の定義箇所を探します。
ここからは、OSに依存したコードに分かれます。まず、Windowsのコードを追います。
ソースファイルopenjdk/hotspot/src/os/windows/vm/os_windows.cppにおいて次のように記述されています。
jlong os::javaTimeNanos() { if (!has_performance_count) { return javaTimeMillis() * NANOSECS_PER_MILLISEC; // the best we can do. } else { LARGE_INTEGER current_count; QueryPerformanceCounter(¤t_count); double current = as_long(current_count); double freq = performance_frequency; jlong time = (jlong)((current/freq) * NANOSECS_PER_SEC); return time; } }
高精度タイマーが利用できない場合、現在時刻をミリ秒分解能で取得し、ナノ秒単位に変換(1000000を乗じる)しています。
高精度タイマーが利用できる場合は、Windows APIのQueryPerformanceCounterで取得したカウント値を、QueryPerformanceFrequencyで取得した周波数で割って秒単位の時刻(計算機起動からの積算時間)を算出し、それをナノ秒単位に変換しています。
has_performance_countの値は、同ソースファイル中で次のように設定されています。
static void initialize_performance_counter() { LARGE_INTEGER count; if (QueryPerformanceFrequency(&count)) { has_performance_count = 1; performance_frequency = as_long(count); QueryPerformanceCounter(&count); initial_performance_count = as_long(count); } else { has_performance_count = 0; FILETIME wt; GetSystemTimeAsFileTime(&wt); first_filetime = jlong_from(wt.dwHighDateTime, wt.dwLowDateTime); } }
Windows APIのQueryPerformanceFrequencyの戻り値が0であると、高精度タイマーがサポートされないことを意味するので、そのときhas_performance_countを0に設定している。
QueryPerformanceCounter APIは、その計算機で利用可能な高精度クロックを1つ選択してカウンタ値を読み出します。PC/AT互換機の場合、おおよそ次のクロックのいずれかが使われます。
PITは、i8254互換の古い1193182Hzの基準周波数で動作する精度の低いタイマーで、カウンタの読み出しは8bitのI/Oポートを用いるためオーバーヘッドが大きく、ほとんど使われることはないでしょう。
ACPI PM Timerは3579545Hzの周波数で動作するタイマーです。カウンタの読み出しは32bitのI/Oポートを用い、1us程度かかります。
HPETは、少なくとも10MHz以上の基準周波数で動作するタイマーです。カウンタの読み出しは32bitのメモリマップドI/Oを用い、1us程度かかります。
Local APICはCPU(コア)に内蔵されバスクロックの分周周波数で動作するタイマーです。メモリマップドI/Oを用います。
TSCは、CPUクロックで動作するタイマーです。カウンタはCPUのレジスタからCPU命令(RDTSCほか)で読み出します。
TSCはCPUコア毎にカウンタを持つためマルチコアで読み出すコアが変わった場合にカウンタ値が大きくずれていることがあります。また、省エネ等で可変クロックの場合にカウンタの更新間隔が変動するといった問題がありました。そのため、マルチコアのHALではTSCではなくHPETが使用される場合が多いようです。しかし、HPETは上述のようにカウンタ値の読み込みに1μ秒程度かかるため、QueryPerformanceCounterの呼び出しコストはTSCに比べて高くつきます。
しかし、その後Constant TSCやInvariant TSCといった改良がされています。
Constant TSCは、コアの周波数を変えても、常に最大のコアクロック/バスクロック比を用いて一定間隔でカウントします。Invariant TSCは、Constant TSCに加え、プロセッサが各種省電力モードに移ってもカウントを継続し、プロセッサ間でのカウンタ値のずれがないように補償しています。
QueryPerformanceCounterがどのタイマを使用するかは、実行する計算機とOSによって変わります。古いシングルプロセッサの計算機ならPITが、Windows XPならACPI PM Timerが、Windows Vista以降はHPETが、シングルプロセッサのHALであればTSCが使われます。
ただし、BIOSの設定やOSのブートパラメータによって変わります。Windows 7 64bitマルチコアの計算機では、デフォルトはLocal APIC Timer(またはLocal APCI TimerとTSCの併用)を使用していると推定され、周波数はおおよそ2MHz台(マシンによって異なる)です。Windows 7のブートパラメータuseplatformclockを指定すると、14.3MHzとなり、HPETを使用していると推定されます。
ブートパラメータの指定方法は、管理者権限でコマンドプロンプトを起動し、次のコマンドを実行します。
C:\> bcdedit /set useplatformclock true
C:\> bcdedit /deletevalue useplatformclock
CPUクロックのカウント値は、通常CPU内部にあるレジスタに格納されています。複数のプロセッサ(マルチコアを含めて)を持つ計算機の場合、CPUによってカウント値にずれが生じます。AMDのデュアルコアにおけるTSC(Time Stamp Counter)のずれとその修正ユーティリティについては以下URLに記載され、またダウンロードできます。
AMDのデュアルコアを使用していて、nanoTimeを使用する場合は、このユーティリティを使用するのがよいでしょう。
参考までに、各OS毎の時間を計測する方法を紹介します。
API名 | 時間種類 | 内容 |
---|---|---|
GetTickCount | 相対時刻 | タイマ割り込み毎にインクリメントされるカウンタ値を取得 |
QueryPerformanceCounter | 相対時刻 | 利用可能な高精度タイマーのカウント値と周波数を取得 |
GetSystemTimeAsFileTime | システム時刻 | タイマ割り込み毎に更新されるシステム時刻を取得 |
TimeGetTime | 相対時刻 | タイマ割り込みで更新されるシステム起動からの経過時間 |
API名 | 時間種類 | 内容 |
---|---|---|
clock | ||
gettimeofday | システム時刻 | UTC1970年1月1日0時0分0秒からの経過時間をマイクロ秒単位で取得 |
API名 | 時間種類 | 内容 |
---|---|---|
gettimeofday | システム時刻 | UTC1970年1月1日0時0分0秒からの経過時間をマイクロ秒単位で取得 |
gethrtime | 相対時刻 | CPUのTICKレジスタから算出したシステム起動からの経過時間をナノ秒単位で取得 |