[ C++で開発 ]

CPUクロックに基づく相対時刻の計測

CPUのクロックに基づき電源ONからの相対時刻を取得する方法を調査し記述します。最近のCPUは、クロックに従ってカウントアップするレジスタを保持しているので、このレジスタの値を読み出すことでCPUが起動してからのクロック数を取得することができます。例えば1GHzの動作周波数のCPUであれば、分解能は1ns(ナノ秒)となります。非常に高精度な分解能です。

2012-09-22
TSCの種類について追記
RDTSCP命令について追記
VisualC++のコンパイラ組み込み関数__rdtsc/__rdtscpについて追記

Intel x86系CPU

RDTSC CPU命令を直接利用

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命令のうち最適と判断した方法で計時しています。

アウト・オブ・オーダー実行とRDTSCP命令

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

Windows OS/Visual C++の組み込み関数__rdtsc

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();
    :
}

Windows OS/Visual C++でのインライン・アセンブラによる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となるようです。

Athlon 64 X2 4200 (2.1 GHz) Windows XPマシンでの実測結果

to_be_measured()の部分にrdtsc()自体を入れて計測した結果、平均250クロック(約110ns)となりました。なお、rdtsc()の実装でCPUID命令を削除したところ、平均65クロック(約30ns)となりました。CPUID命令に約185クロック(約81ns)要していたことになります。

参考までにWindows APIのQueryPerformanceCounter()を実行したところ、約4000クロック要していました。2.2GHzで4000クロックなので約1.8μ秒となります。ちょっと重いシステムコールといった処理です。

Windows OS/GNU CコンパイラでのRDTSC命令

Cygwin等のWindows OS上でGCCを使用した場合の方法です。

T.B.D.

Linux OS/GNU CコンパイラでのRDTSC命令

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の場合、<要調査>

参考資料