前回は時間をテーマに戯れましたが、今回も前回に引き続き時間に玩ばれましょう。

さて、前回は低レベル層での時刻カウントでしたが、今回はよりレベル階層を上層に上げて、OS が提供している関数レベルで戯れてみます。
ぶっちゃけた話、OS の API をぶったたくだけです



さて、どのような関数があるか列挙してみますか・・・
数多くある時刻カウント系の関数ですが、まずはわたくしが知っている限りを諳んじて見ましょう。
(UNIX のシステムコールや 非AT互換 のメーカー固有の低レベル API は無視して、Windows 系のみ抑えておきます)
clocktime.hタスクプロセス単位で消費された CPU 消費時間を取得します。
取得時間を秒に換算するには、マクロの CLOCKS_PER_SEC で割算しなければなりません。
timeGetTimemmsystem.hシステム(Windows)が起動してから経過した時刻をミリ秒単位で取得します。
GetTickCountWinbase.hシステム起動から経過時間をミリ秒単位で取得します。
ただし取得した時刻の制度は、システムタイマの分解能に依存しています。
timeGetSystemTimemmsystem.hシステム起動から経過時間をミリ秒単位で取得します。
動作仕様はtimeGetTime 関数と同じであったりします・・
QueryPerformanceCounterWinbase.h使用しているハードが高分解能パフォーマンスカウンタを持っている場合、現在のカウンタ値を取得します。
基本的に Intel の Pentium 以降の MPU ならたぶん大丈夫でしょう・・・
QueryPerformanceFrequencyWinbase.h 高分解能パフォーマンスカウンタが使える環境下で、そのカウンタの周波数を取得します。
要するに単位秒辺りのカウント数を取得する関数です。
ただし、現行の仕様ではシステム起動時にセットされるようですので、更新頻度は全くありません。
もしこの関数を使用するのであれば、プロセス起動時に一度グローバル値として取得しておけばいいでしょう。
あやゃ、六つしか思いつかなかった。
では、適当に調べてみましょうか
GetCurrentTime
GetMessageTime
ありゃ、これぐらいしかないの?意外と少ないもんですね。
DirectMusic などにもクロック制御用に使えそうな関数がありましたが、今回は敢て割愛しました・・・っていうか、DirectX は知らんので


さて、いろいろと時刻取得に関連する関数があるのが判ったところで話を先に進めましょう。

実際にコードを通して上記関数群のいくつかの動作を覗って見ませう。


times.cpp

 #include <windows.h>
 #include <mmsystem.h>
 #include <stdio.h>
 #include <time.h>
 #define rdtsc __asm __emit 0fh __asm __emit 031h
 main(){
   LARGE_INTEGER PerFreq;
   DWORD         Freq;
   QueryPerformanceFrequency(&PerFreq);
   Freq = PerFreq.LowPart;
   printf("System calculated by %uMhz\n", Freq);

   {
       unsigned __int64 nCyclesCount;
       void* pbuf = &nCyclesCount;
       pbuf = &nCyclesCount;
       __asm{
         push eax
         push ebx
         push edx
         rdtsc
         mov ebx, pbuf
         mov [ebx],eax
         mov [ebx+4],edx
         pop edx
         pop ebx
         pop eax
       }
       printf("RDTSC                         "
              "- %I64u Cycles\n", nCyclesCount);
       printf("RDTSC / Mhz                   "
              "- %I64u [s]\n"
              , nCyclesCount/(unsigned __int64)Freq);   
   }

   DWORD   dwCount;
   {
       dwCount = GetTickCount();
       printf("GetTickCount                  "
              "- %u [ms]\n", dwCount);
       printf("GetTickCount / 1000           "
              "- %u [s]\n", dwCount/1000);
   }
   {
       dwCount = timeGetTime();
       printf("timeGetTime                   "
              "- %u [ms]\n", dwCount);
       printf("timeGetTime / 1000            "
              "- %u [s]\n", dwCount/1000);
   }
   {
       QueryPerformanceCounter(&PerFreq);
       printf("QueryPerformanceCounter       "
              "- %I64u Cycles\n", PerFreq.QuadPart);
       printf("QueryPerformanceCounter / Mhz "
              "- %I64u [s]\n"
              , PerFreq.QuadPart/(unsigned __int64)Freq);  
   }
   getchar();
   return 0;
 }

今回は随分と手を抜いたコードですが、シンプルなので詳細説明は不要かな?
とりあえずサックっと実行してみますと次のような結果が得られました。


 System calculated by 601450000Mhz
 RDTSC                         - 13239572201938 Cycles
 RDTSC / Mhz                   - 22012 [s]
 GetTickCount                  - 22011781 [ms]
 GetTickCount / 1000           - 22011 [s]
 timeGetTime                   - 22011781 [ms]
 timeGetTime / 1000            - 22011 [s]
 QueryPerformanceCounter       - 13239572768338 Cycles 
 QueryPerformanceCounter / Mhz - 22012 [s]



それではコード内容を解説しませう。

まず用いたタイマー系のコマンドを発行順に列挙します。
QueryPerformanceFrequency
rdtsc
GetTickCount
timeGetTime
QueryPerformanceCounter
以上、五つの関数を用いてみました。

先に進む前に簡素に関数の役割を紹介しておきましょう。
  • QueryPerformanceFrequency
  • 周波数を取得します。
    つまりは一秒間にカウントされるクロックサイクルを取得するのですが、どうもこの値はシステム起動時にレジストリー上の HKEY_PEFORMANCE_DATA に格納されるらしいです。
  • rdtsc
  • 前回紹介した ReaD-Time Stamp Counter です。
    マシン起動時からクロックサイクル毎にカウントされ続けられる値です。
  • GetTickCount
  • システム起動後から経過した時間をミリ秒単位で取得します。
    値算出にはレジストリー上の HKEY_PERFORMANCE_DATA キー内の System Up Time からカウンタを取得して算出されるそうです。
  • timeGetTime
  • Windows が起動してから経過した時間をミリ秒単位で取得します。
  • QueryPerformanceCounter
  • Win32APIでサポートされているRDTSC相当のもの
    とりあえず如何でしたか?

    クロックサイクルが正確でないのは致し方ありませんが・・・・、っていうか、NT系の OS を用いているならば、同値をレジストリ上から取得することもできますが、この値も真値からは離れています。
    MPUに対する真なるクロックカウンタはハード上で流れている電気の状況によって逐次変動しており、さらに上位のドライバ(BIOSレベルの)の動作によってもクロック周波数は変わるし・・・まぁ、どうでもいいか
    ちなみに該当レジストリは以下のキーを参照してみれば・・・私の環境の場合
    ハイブ:HKEY_LOCAL_MACHINE
     キー:DESCRIPTION\System\CentralProcessor\0
     値名:~MHz
      値:0x00000259(601)
    って、なっております。・・・なるほど、システム的には601MHzであることが判ります。
    ちなみに BIOS レベルで設定されている値は 100MHz(FSB)×6.0(Mult.) になっています。
    ここで生じた差額 1MHz やさらには QueryPerformanceFrequency 関数が返す 601,450,000MHz との差額 1,450,000MHz が、M/B に期待されている真値とのズレなわけです。(もっともクロックサイクルの設定時の状況によってこの値は変動します)
    ズレの原因を特定することはできませんが、いい加減な造りのAT互換系機に正確無比なクロックサイクルを求めるのも酷な話でしょう。・・・つぅーか、私の PC の M/B は MPU に対して過負荷な電圧を掛けているんかいな?
    さて値のズレに対しては、 http://www.wheaty.net/ の Matt Pietrek 氏などは以下のようにコメントしております。
    In the end, the granularity of QueryPerformanceCounter seemed perfectly adequate for the durations under consideration.
    この意見には私も同感します。だって、クロックサイクルなんぞ今日日の マルチタスクOS 上では大雑把に当っていればOK!
    べつに 1MHz(1μ秒)如きズレていようがシステム的には問題ナッシングですよ!
    だってマルチタスキングな OS を使っているんだもん、OS のタスクディスパッチャによってウェイトバイアスかかっているもん・・・しかし、このズレがときにはとんでもないトラブルを生じさせる可能性があることは否定しませんが・・・特に外部デバイスとの同期処理とか・・通常意識することはないでしょう



    話を戻して、コードを追っていきましょう。
    さて、クロックサイクルが得られたら次に RDTSC を用いて MPU 側で管理されているカウンタ値を取得します。
    通常、このような MPU 固有のコードを書く際の作法としては、前以てシステムがサポートしている機能かどうか判明しておくのがアプリケーションのマナーですが、今回は考慮を割愛しています。
    補足:RDTSCが使えるか?

    :実行環境で RDTSC のサポートの有無をどうやって知ればいいの?
    :つこーている本人に尋ねる!これじゃだめ?

    だめだよね・・・話題が必然とアセンブラに偏ってしまうので、あまり話題にしたくないのですが

    さくっと簡単に(手を抜いて)概略を説明しておきます。
    さて、覚悟してください。少しの間、アセンブラと戯れます。
    戯れたくない人は、先に進んじゃってください




    さて、RDTSC 機能の有無を調べるには、CPUID コマンドを発行して、その戻り値から判断すれば判ります。
    具体的には以下のようなコードで十分でしょう。
    IsRDTSC.cpp
    
     #include <windows.h>
     #include <stdio.h>
     #define CPUID __asm __emit 0fh __asm __emit 0a2h  
     int main(void){
        DWORD     dwCPUIDEDX;
        _asm {
            mov   EAX, 1
            CPUID
            mov  dwCPUIDEDX, EDX
            and  dwCPUIDEDX, 10h
        }
        if (dwCPUIDEDX){
            puts("OK\n");
        } else {
            puts("NG\n");
        }
        return 0;
     }
    
    では、コードの内容を説明しておきましょう。
    まず、accumulator レジスタに( EAX=1 )をセットします。
    その後、にっこり笑みを浮かべながら CPUID コマンドを発行、結果セットが Data レジスタの値に機能フラグ (Feature flags) として格納されます。
    後は、ここの 5bit 目にお目当ての答えが隠れています。(該当 bit は Time Stamp Counter の有無をフラグ立てています)
    Intel の機能仕様などを伺うと、
    The RDTSC instruction is supported including the CR4.TSD bit for access/privilege control
    って、書いています。詳細は Intel の黒い本(仕様書)を読んでください。(大きい書店に売っていますので)

    差し当たっては Intel のサイトから入手できる技術情報でも参考にしてみては?
    http://developer.intel.com/design/pentiumii/applnots/241618.htm
    ftp://download.intel.com/design/PentiumII/applnots/24161817.pdf
    上記の URL で入手した資料第14項にフラグの内訳表が記載されています。



    CPUID コマンドの仕様を列挙しましょう。(基本的には上の URL で入手できる PDF の第8項・第9項に書かれている内容を邦訳しただけにすぎません)
    Input(EAX)Output
    0EAX -> CPUID コマンド認識可能最大値
    EBX、EDX、ECX -> ベンダID文字列(識別文字列)
    1EAX -> プロセッサ署名、若しくは 96bit プロセッサシリアル番号の上位 32bit
    EDX -> 機能フラグ
    EBX[7:0] -> ブランド識別子
    EBX[31:8] -> 予約済み
    2EAX,EBX,ECX,EDX -> プロセッサ構成値
    3EDX:ECX -> 96bit プロセッサシリアル番号の下位 96bit
    認識可能最大値まで
    (4≦MAXVAL)
    予約済み
    最大値以上EAX:EBX:ECX:EDX -> 未定義(使用不可)
    さて、これで実行環境の MPU の状態がわかったはずですが、実は一つ問題があります。
    それは・・これだけの考慮では不十分なのです。
    なぜならば、CPUID コマンド自体をサポートしていない MPU が存在するからです。つまりは RDTSC を使いたいから、前以て実行環境でサポートされているかどうか調べるために CPUID を用いたとしても、肝心の CPUID 自身が対象の MPU でサポートされているどうか、その有無を調べないとならないわけです。

    そこで続いては、この CPUID のサポートの有無を調べる手段を紹介しておきましょう。

    調べ方は非常に簡単です。
    x86系 IntelMPU では、486シリーズ以降の EFLAGS レジスタの 21bit 目で CPUID が使用できるか判断することができます。
    要するにEFLAGS レジスタのフラグを参照することでサポートの有無を見分けるのです。
    さて実装を紹介しましょう・・・基本的には EFLAGS レジスタの 21bit がフラグ立てられているかどうか判定するだけの代物です。
    IsCPUID.cpp
    
     #include <windows.h>
     #include <stdio.h>
     int main(void)
     {
       DWORD    cputype;
       _asm {
          push    eax          ; Original EAX を スタックに退避
          push    ecx          ; Original ECX を スタックに退避
    
          pushfd               ; Original EFLAGS を退避
          pop     eax          ; Original EFLAGS を取得
          mov     ecx, eax     ; Original EFLAGS を ECX に保管
          xor     eax, 200000h ; flip ID bit in EFLAGS
          push    eax          ; 新しい EFLAGS 値をスタック上に退避 
          popfd                ; 現在の EFLAGS 値に変更する
          pushfd
          pop     eax          ; 新しい EFLAGS を EAX に取得
          xor     eax, ecx     ; can't toggle ID bit,
          mov     cputype, eax
    
    
          pop     ecx
          pop     eax
       }
       if (!cputype){
          puts("NG");
       }
       return 0;
     }
    
    上のコードに関して EFLAGS の使い方等は Intel のドキュメントの第6・7項を参考にしてください。

    これで対象の MPU が CPUID が使えるかどうか判り、さらに RDTSC の有無もわかるはずです。



    ところで、ここまで面倒くちゃいアセンブラを用いましたが、実は WinAPI 上に手取り早く RDTSC の有無をチェックする API が用意されているんです。
    つまりは自家製のアセンブラで苦労するよりも OS が提供している API を一発叩けば、知りたい情報が取得できるわけで、後者の方がコスト的に安くつきます。


    では、取り纏めとして WinAPI を一発紹介しましょう!
    用いるべき関数は IsProcessorFeaturePresent 関数になります。
    正確には
    BOOL IsProcessorFeaturePresent(
      DWORD ProcessorFeature  // プロセッサ機能を指定
    );
    引数 内容
    PF_FLOATING_POINT_PRECISION_ERRATA まれに浮動小数点精度エラー発生可能性があるか(Pentium)
    PF_FLOATING_POINT_EMULATED 浮動小数点演算がソフトウェアエミュレータを使ってエミュレートされるか
    Windows 2000Windows NT 4.0 において若干動作がことなる)
    PF_COMPARE_EXCHANGE_DOUBLE 比較置換二重演算に対応しているか(Pentium、MIPS、および Alpha)
    PF_MMX_INSTRUCTIONS_AVAILABLE MMX 命令セットに対応しているか
    PF_ALPHA_BYTE_INSTRUCTIONS Windows 2000 以降:Alpha のバイトロード命令とバイトストア命令に対応しているか
    PF_XMMI_INSTRUCTIONS_AVAILABLE XMMI 命令セットに対応しているか
    PF_3DNOW_INSTRUCTIONS_AVAILABLE 3D-Now 命令セットに対応しているか
    PF_RDTSC_INSTRUCTION_AVAILABLE RDTSC 命令セットに対応しているか
    PF_PAE_ENABLED プロセッサは PAE-enabledか

    後は関数からの戻り値が0以外で、対象機能がサポートされているかどうか判明します。
    しかしながらこの関数にも欠点がありまして、NT カーネル系以外のシステムではサポートしていないのです。
    なんとも中途半端な仕様ですが、まぁ、これはこれ・・・・これからの MS 製 OS は NT カーネルベースになるからいいかぁ
    でも、Intel の次世代 64bit MPU が市場に出つつあるので、これらの命令セットや関数を用いたプログラムは書き直しを覚悟しなければなりませんね




    さて、ずいぶん前に戻って、話題を再開しましょう。
    ・・・・なんの話をしていったんだけ?
    そうそう、RDTSC の次に発行するコードは GetTickCount 関数になります。
    この関数に関してはただ呼び出すだけで、システムが起動してからの時刻をミリ秒単位で返します。
    意外と制度的には落ちる関数ですが、ネットワーク関連のプログラムを書いていると同関数には大変ご厄介になりますね。

    次に timeGetTime 関数でシステム起動時点からの時刻を取得、最後に QueryPerformanceCounter で PC 起動時からのクロックカウンタを取得しますがぁ、意外とこのカウンタ値自体もあてにならないもんで、外部デバイスなどとの会話の動機を取る際、自身のクロックカウンタのみで同期制御をしていると痛い目をみます・・・はい、わたくし、数回見ました。

    もっともAT互換機自身に積まれているクリスタルが安物だから固体差はあるは、温度などの動作状況によってクリスタルのサイクルがずれるので気にしても仕方がないのでは?ってな、意見もありますので、外部環境との会話を行うようなプログラムを作る際には十分考慮したうえで、もし同期シグナルが存在するのならばそちらを用いましょう。
    ところで PC はいつまでクリスタルに依存しているのでしょうか?
    バスのスピードが MHz 単位である限り懸念するほどのことではないのでしょうが、近い将来 GHz 環境が標準になれば、クリスタル如きでは同期制御の精度が保てないのでは?となると同期シグナルはセシウムやルビジウムを使うことになるのかな?
    でも、まじで、GHz の MPU がコンシューマで販売されているし、市販レベルで GMbps クラスの NIC が万のオーダーで販売されているから近い将来、確実に外部 I/F の同期速度は GMHz になるのでは?ってことは Baud Rate はとんでもないサイズになるんかいな?


    さて、ずいぶん脱線してしまったが、以上で時間との戯れはやめます。
    では、また


    目次に戻る
    全文メニュー
    サイトの玄関


    ご意見、ご感想、ご要望等々はこちらのメールアドレス takamegu@lycos.ne.jp までご一報ください。
































































    最後に QueryPerformanceCounter 関数について一言
    MS のドキュメント
    http://www.microsoft.com/Japan/developer/visualc/techmat/feature/optcodedev/
    「Microsoft Visual C++ 6.0 による最適化コードの開発」
    このドキュメントの「Visual C++ を使ったタイミングとプロファイリング」の項目に気になる文章があったので抜粋しますが
    マルチプロセッサのコンピュータを使っている場合、どのプロセッサを呼び出しても問題はありません。
    ただし、BIOS または HAL(ハードウェアエミュレーションレイヤ)のバグが原因で、異なったプロセッサを呼び出すと、異なった結果を取得する可能性があります。
    特定のスレッドに対するプロセッサの親和性を指定する(特定のスレッドでただ 1 つのプロセッサを使うよう指定する)には、SetThreadAffinityMask 関数を使います。
    そう、ここまでのおはなしでわたくしマルチプロセッサ環境のことを考慮に入れていませんでした!
    しかし、とりあえず今回はマルチプロセッサに対する考慮は割愛させてください。いずれまたどこかで機会がありましたらご説明しましょう。