[ C++で開発 ]

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

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

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も内部でこのRDTSC命令を利用しているそうです。

使用上の注意

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です。
http://www.amd.com/jp-ja/Processors/TechnicalResources/0,,30_182_871_13118,00.html

Windows OS/Visual C++でのRDTSC命令

インライン・アセンブラを用いて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;
}