時刻取得・時間計測

IA-32、AMD64、Intel 64 のいずれかのアーキテクチャで TSC (Time Stamp Counter) を用いる例。

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
static inline uint64_t rdtscp(void)
{
    uint64_t rax, rdx, aux;
    __asm__ volatile ("rdtscp\n"
            : "=a" (rax), "=d" (rdx), "=c" (aux)
            :
            : );
    return (rdx << 32) + rax;
}

int cmp(const void *a, const void *b) { return *(int*)a - *(int*)b; }

int main(int argc, char **argv)
{
    int array[] = { 5, 10, 111, 3, 16, 28, 591, 360, 1, 0, 83, 72, 71, 33, 55 };
    uint64_t start_time, end_time;
    start_time = rdtscp();
    qsort(array, sizeof(array)/sizeof(int), sizeof(int), cmp);
    end_time = rdtscp();
    printf("start TSC: %PRI64u\nend TSC: %PRI64u\nend - start: %PRI64u\n", start_time, end_time, end_time - start_time);
    return EXIT_SUCCESS;
}

元々は rdtsc が用いられていたが、Out-of-Order 実行を実装した P6 マイクロアーキテクチャ (Pentium Pro) 以降では命令実行順が保証されないため rdtsc の前に cpuid を挿入してシリアライズすることが定番であった。rdtscp では cpuid を挿入せずともシリアライズされるほか、aux として取得できる値によって、マルチコアプロセッサーの場合にどのプロセッサのカウンタを取得したか把握可能になっている(TSC はコア毎にべつべつに積算される)。上記コードではこれを実施していないが、本来は全てのコアの TSC の基準値を取得した上で、計測時には auxの値を見て基準値との差分を取る、といった操作によってコア毎の TSC のブレを考慮しなければならない。

TSC には Constant TSC、Invariant TSC という派生があり、それぞれ、クロック変動による TSC 変動の補償、Constant TSC に加えて電力制御による変動の補償、ということが可能になっており、これらが利用できるかは cpuid 命令によって確認できる。

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdbool.h>
int main(int argc, char **argv)
{
    uint64_t rdx;
    bool has_tsc, has_rdtscp, has_nonstop_tsc;
    
    __asm__ volatile("cpuid" : "=d"(rdx)
                             : "a"(0x1)
                             : "rbx", "rcx");

    has_tsc = (bool)((rdx >> 4) & 0x1); // CPUID Fn0000_0001.EDX[4] == 1
    if (has_tsc)
        printf("this processor has TSC.\n");
    else {
        printf("this processor doesn't have TSC.\n");
        exit(EXIT_SUCCESS);
    }
    
    
    __asm__ volatile("cpuid" : "=d"(rdx)
                             : "a"(0x80000001)
                             : "rbx", "rcx");
                             
    has_rdtscp = (bool)((rdx >> 27) & 0x1); // CPUID Fn8000_0001.EDX[27] == 1
    if (has_rdtscp)
        printf("this processor has rdtscp instruction.\n");
    else {
        printf("this processor doesn't have rdtscp instruction.\n");
        exit(EXIT_SUCCESS);
    }
    
    __asm__ volatile("cpuid" : "=d"(rdx)
                             : "a"(0x80000007)
                             : "rbx", "rcx");
                             
    has_nonstop_tsc = (bool)((rdx >> 8) & 0x1); // CPUID Fn8000_0007.EDX[8] == 1
    
    if (has_nonstop_tsc)
        printf("You can use Invariant TSC.\n");
    else
        printf("You cannot use Invariant TSC.\n");
        
    return EXIT_SUCCESS;
}

rdtscp 命令は AMD64 では K8 マイクロアーキテクチャ (Hammer) の Opteron rev F 以降、Intel 64 では Nehalem マイクロアーキテクチャの Core i7 以降に実装されている。

ちなみに、TSC は P5 マイクロアーキテクチャ (Pentium) 以降から追加され、Constant TSC は NetBurst マイクロアーキテクチャ (Pentium 4) 以降、Invariant TSC は Nehalem マイクロアーキテクチャ以降追加された。

AMD64 では K10 マイクロアーキテクチャ (Barcelona)、つまり Phenom から Constant TSC になったようだが、これはほぼ Invariant TSC と同じ機能のようで、Intel 同様に CPUID Fn8000_0007.EDX[8] を見れば確認できる。

Constant TSC に対応するかどうかだけを確認する機能は存在しない。

TSC から時間への変換

TSC は基本的にはクロック毎にカウントする積算値なので、時間に変換する必要がある。仮に 1GHz クロックでかつ一切クロック変動がなく、シングルコアのプロセッサーが存在するとして、このプロセッサーが 1 クロックあたりにかかる理論値は 1/1,000,000,000 = 1 ns、つまり 1 ナノ秒である。つまり、TSC で計測した時間が 200,000 だったとしたら、200,000 * (1/1,000,000,000) = 2 μs、つまり 2 マイクロ秒となる。

ただし実際には Invariant TSC を使う環境ではクロック変動を考慮しなければならない。そこで、Invariant TSC Frequency というものがプロセッサーには設定されており、Invariant TSC はこれを基準にする。

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdbool.h>
static inline uint64_t rdmsr(uint64_t addr)
{
    uint64_t rax, rdx;
    __asm__ volatile ("rdmsr\n"
            : "=a"(rax), "=d"(rdx)
            : "c"(addr)
            : );
    return (rdx << 32) + rax;
}

void get_invariant_tsc_freq(void)
{
    uint64_t invariant_tsc_freq = ((rdmsr(0xce) >> 8) & 0xff);

    printf ("Invariant TSC Freq is %PRI64u x 100MHz", invairant_tsc_freq);
    
    return EXIT_SUCCESS;
}

しかし上記コードは実用上問題がある。rdmsrMSRs という特殊レジスタにアクセスするため特権命令である。つまり、アプリケーションには組込めない。

とはいえ、Invariant TSC Frequency はだいたいいつも Intel CPU では Brand String の末尾に記載されている周波数(=定格周波数)と合致するので、その値を使ってしまうのも手かもしれない。ただし、これが正しい Invariant TSC Frequency であることは保証しない。この方法で定格周波数を取得する方法は次のとおり。

CPU の定格クロックを取得する(IA-32,Intel 64 限定)

言語標準機能を用いる例

ISO/IEC 9899:2011 (C11)
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int cmp(const void *a, const void *b) { return *(int*)a - *(int*)b; }

int main(int argc, char **argv)
{
    int array[] = { 5, 10, 111, 3, 16, 28, 591, 360, 1, 0, 83, 72, 71, 33, 55 };
    struct timespec start, end;
    timespec_get(&start, TIME_UTC);
    qsort(array, sizeof(array)/sizeof(int), sizeof(int), cmp);
    timespec_get(&end, TIME_UTC);

    printf("time: %10ld.%9ld", end.tv_sec - start.tv_sec, end.tv_nsec - start.tv_nsec);

    return EXIT_SUCCESS;
}
ISO/IEC 14882:2011 (C++11)
#include <cstdlib>
#include <array>
#include <algorithm>
#include <chrono>
#include <iostream>

int main(int argc, char **argv)
{
    std::array<int, 15> data = { 5, 10, 111, 3, 16, 28, 591, 360, 1, 0, 83, 72, 71, 33, 55 };
    auto start_time = std::chrono::system_clock::now();
    std::sort(data.begin(), data.end());
    auto end_time = std::chrono::system_clock::now();

    auto get_time = std::chrono::duration_cast<std::chrono::nanoseconds>(end_time - start_time);
    
    std::cout << "time: " << get_time.count() << " nsec" << std::endl;

    return EXIT_SUCCESS;
}

はっきりいってこれらを利用するほうが TSC を利用するより懸命だと思われる。また、次に説明する OS 依存のタイマーについてプラットフォームの差を吸収してくれる。

OS のタイマー

  • タイマー(秒、あるいはミリ秒単位)
    • Unix(POSIX): time(2)gettimeofday(2)
    • macOS: NSTimer クラス
    • Windows: timeGettime()GetTickCount など
  • 高精度タイマー(ナノ精度、ハードウェア依存)
    • Unix: clock_gettime(2)
    • macOS: mach_absolute_time()
    • Windows: QueryPerformanceCounter()

なお、gettimeofday(2) は非推奨の API であり、代替として clock_gettime(2) を利用するようになっている。clock_gettime(2) では clock_getres(2) を用いることによってタイマの精度を取得できる。

macOS の API がいかにも Mach microkernel 由来っぽいのはおもしろい。

TSC 以外のハードウェアのタイマー

いわゆる PC/AT 互換といわれるハードウェアでは、チップセットなどにもタイマーが実装されていることがある。主なタイマーは次のとおり。

  • PIT (Programmable Interval Timer)
  • RTC (Real Time Clock)
  • HPET (High Precision Event Timer)
  • ACPI PM Timer
    • ACPI ... Advanced Configuration and Power Interface, PM ... Power Management
  • Local APIC Timer
    • APIC ... Advanced Programmable Interrupt Controller

この中では HPET が比較的よく見掛けるように思われる。というのも、計測だけでなく OS を自作しスケジューラーなどタイマー割り込みの必要な機能を実装する際、値を積算することしかできない TSC だけでは無意味で、何かしらの時間間隔で割り込みを発生させるような機能もまた必要だからである。よって、HPET や Local APIC Timer なども必要不可欠となる。ただし、HPET のほうがレガシーである。

このブログの人気の投稿

ssh-rsa,非推奨のお知らせ

Makefileの基本的な書き方について

Oculus Quest と Virtual Desktop でモニタが一枚しかない部屋でマルチモニタを実現したい