[ C++で開発 ]
CPUのクロックに基づき電源ONからの相対時刻を取得する方法を調査し記述します。最近のCPUは、クロックに従ってカウントアップするレジスタを保持しているので、このレジスタの値を読み出すことでCPUが起動してからのクロック数を取得することができます。例えば1GHzの動作周波数のCPUであれば、分解能は1ns(ナノ秒)となります。非常に高精度な分解能です。
Intel x86系のCPU(AMDのAthlon等も含む)では、CPUクロックごとに加算される64bitのタイムスタンプカウンタ(IA32_TIME_STAMP_COUNTER_MSR:通称TSC_MSR)があります。これをRDTSC(Read
Time Stamp Counter)命令を使って読み出すことで、CPUクロックと同じ分解能を持つ精度のタイマが実現できます。
64bit値のカウンターなので、カウンター溢れは事実上無視してもよい期間(年オーダー)です。
Windows OSのQueryPerformanceCounter APIは、PIT, ACPI PM Timer, HPET, およびこのRDTSC命令のうち最適と判断した方法で計時しています。
Pentium Proおよびそのアーキテクチャを継承したPentium II以降では、アウト・オブ・オーダー実行によりRDTSC命令の実行タイミングがプログラム上の順序とずれる可能性があります。
そのため、RDTSC命令の前にCPUID命令などを実行し、RDTSC命令を逐次的(シリアライズ)に実行するテクニックが使われます。
RDTSCP命令は、CPUID命令などを組み合わせなくてもシリアライズするようになります。ただし、RDTSCP命令を持つCPUは限られているので、使用にあたってはCPUがサポートしているかを調べる必要があります。
CPUの世代・種類によってTSCの機能に違いがあることが分かりました。
TSC種類 | 概要 | 周波数変動への補償 | サスペンドへの補償 | マルチコアでの補償 | おおよその目安 |
---|---|---|---|---|---|
TSC | CPUの動作クロック毎に積算されるカウンタ | なし(ずれる) | なし(ずれる) | なし(ずれる) | Pentium以降のプロセッサ |
Constant TSC | CPUの動作クロックが変動してもカウンタ積算間隔は一定となる補償機能を追加したTSC | あり | なし(ずれる) | なし | Pentium4、Core/Core2、AMD Phenom |
Invariant TSC | ACPIのP/C/T状態すべてにおいてカウンタ積算間隔が一定で動作する補償機能を追加したTSC | あり | あり | あり | Core i7、Phenom II |
TSCを持っているかどうか、Invariant TSCかどうかは、CPUID命令を発行して調べます。Constant TSCを識別する情報はCPUIDでは取得できなさそうです。
CPUIDで取得する内容の定義はCPUメーカーの公開する情報を見るのがよいでしょう。
その他CPUID情報
TSCについては、Invariantが導入されるまでは、Constantであっても動作に問題がありました。
RDTSCの値が信頼できないという問題指摘もあるようです。
http://www.atmarkit.co.jp/flinux/rensai/watch2005/watch12b.html
デュアルコア/複数CPUのマシンではCPUごとにTSC(Time Stamp Counter)がずれる、Athlon x2ではHLT命令を発行したコアでTSCが停止するといった課題もあるようです。
http://www.atmarkit.co.jp/flinux/rensai/watch2006/watch02b.html
この場合、一つのコア(CPU)でスレッドが実行されるようにOSのスケジューラを制御する必要があります。これを指定しなかった場合にデュアルコアでの計測結果がかなり振れていました。SetThreadAffinityMask、SetProcessAffinityMaskかSetThreadIdealProcessorといったAPIを使用します。
追記)AMDから、"AMD Dual-Core Optimizer"がリリースされています。コア間で定期的にTSCを同期することでコア間のTSCずれを回避するものです。なお、対象はWindows
OSです。あくまで初期のTSCしか持たないCPU用です。
http://www.amd.com/jp-ja/Processors/TechnicalResources/0,,30_182_871_13118,00.html
Visual C++ 8.0(Visual Studiuo 2005)以降は、コンパイラ組み込み関数(Compiler Intrinsics)で用意される__rdtsc関数が利用できます。__rdtscp関数はVisual C++ 9.0(Visual Studio 2008)以降は、__rdtscp関数も利用できます。
#include <intrin.h> #pragma intrinsic(__rdtsc) int main() { unsigned __int64 counter; counter = __rdtsc(); : }
Visual C++ 7.1(Visual Studio .NET 2003)までは、インライン・アセンブラを用いてC++上にCPU命令を使用するコードを記述します。
RDTSC命令は、カウンタの値(64bit)をEAXレジスタ(下位32bit)、EDXレジスタ(上位32bit)に格納します。
rdtsc.h
#ifndef RDTSC_H #define RDTSC_H inline __int64 __fastcall rdtsc() { __asm { cpuid rdtsc } }; #endif /* RDTSC_H */
__int64 は、VC++の拡張型で64bit符号付整数を示します。
__fastcallは関数呼び出し規約で若干高速な呼び出しを指定します。
cpuid命令は、RDTSC命令が先行する他の命令を追い越す(リオーダリング)されないために入れています。しかしながら、CPUID命令は非常に重い命令なので(数百サイクルらしい、下記URL参照)、計測にあたって留意すべき事項です。
http://www.atmarkit.co.jp/flinux/rensai/watch2006/watch07a.html
CPUID命令の処理時間については、最初の2回は時間がかかるので3回目以降を使用せよとの記述がたしかIntelのRDTSCに関する資料にありました。
C言語での関数において64bit整数の戻り値は、EAXレジスタ/EDXレジスタに格納して関数呼び出し元に渡す規約なので、RDTSC命令の結果がそのまま関数の戻り値と扱うことができます。
計測例
#include "rdtsc.h" #include <stdio.h> int measure_func() { SetThreadAffinityMask(GetCurrentThread(), 1); __int64 start = rdtsc(); to_be_measured(); __int64 stop = rdtsc(); printf("measured time : %I64d [clock]\n", stop - start); return 0; }
SetThreadAffinityMaskで、指定したスレッドを実行可能なプロセッサを指定(限定)します。デュアルコアの場合この指定がないときに結果がぶれます(1秒間スリープしてクロックを計測するとき等)。
printfの書式指定にある%I64は、64bit符号付整数を指定するVC++拡張の書式I64です。符号なしのときはUI64となるようです。
to_be_measured()の部分にrdtsc()自体を入れて計測した結果、平均250クロック(約110ns)となりました。なお、rdtsc()の実装でCPUID命令を削除したところ、平均65クロック(約30ns)となりました。CPUID命令に約185クロック(約81ns)要していたことになります。
参考までにWindows APIのQueryPerformanceCounter()を実行したところ、約4000クロック要していました。2.2GHzで4000クロックなので約1.8μ秒となります。ちょっと重いシステムコールといった処理です。
Cygwin等のWindows OS上でGCCを使用した場合の方法です。
T.B.D.
Linux OS上でGCCを使用した場合のRDTSC命令を使ったタイムスタンプカウンタ取得方法です。
#ifndef RDTSC_H_ #define RDTSC_H_ inline unsigned long long rdtsc() { unsigned long long ret; __asm__ volatile ("rdtsc" : "=A" (ret)); return ret; } #endif /* RDTSC_H_ */
使用例
#include "rdtsc.h" #include <stdio.h> int measure_func() { unsigned long long start = rdtsc(); to_be_measured(); unsigned long long stop = rdtsc(); printf("measured time : %I64d [clock]\n", stop - start); return 0; }
クロックの分解能(精度)は、結局のところハードウェアに依存してしまいます。
PC AT互換機で利用可能なタイマーデバイスには主に次があります。
Linux(RedHat系)の場合、利用可能なタイマーデバイスは次で調べることができます。
$ cat /sys/devices/system/clocksource/clocksource0/available_clocksource tsc hpet acpi_pm $
Windowsの場合、<要調査>