web-dev-qa-db-ja.com

GCC x86でRDTSCを使用してクロックサイクルをカウントする方法は?

Visual Studioを使用すると、以下に示すように、プロセッサからクロックサイクルカウントを読み取ることができます。 GCCで同じことを行うにはどうすればよいですか?

#ifdef _MSC_VER             // Compiler: Microsoft Visual Studio

    #ifdef _M_IX86                      // Processor: x86

        inline uint64_t clockCycleCount()
        {
            uint64_t c;
            __asm {
                cpuid       // serialize processor
                rdtsc       // read time stamp counter
                mov dword ptr [c + 0], eax
                mov dword ptr [c + 4], edx
            }
            return c;
        }

    #Elif defined(_M_X64)               // Processor: x64

        extern "C" unsigned __int64 __rdtsc();
        #pragma intrinsic(__rdtsc)
        inline uint64_t clockCycleCount()
        {
            return __rdtsc();
        }

    #endif

#endif
16
Johan Råde

Linuxの最近のバージョンでは、gettimeofdayにナノ秒のタイミングが組み込まれます。

本当にRDTSCを呼び出したい場合は、次のインラインアセンブリを使用できます。

http://www.mcs.anl.gov/~kazutomo/rdtsc.html

#if defined(__i386__)

static __inline__ unsigned long long rdtsc(void)
{
    unsigned long long int x;
    __asm__ volatile (".byte 0x0f, 0x31" : "=A" (x));
    return x;
}

#Elif defined(__x86_64__)

static __inline__ unsigned long long rdtsc(void)
{
    unsigned hi, lo;
    __asm__ __volatile__ ("rdtsc" : "=a"(lo), "=d"(hi));
    return ( (unsigned long long)lo)|( ((unsigned long long)hi)<<32 );
}

#endif
25
Andrew Tomazos

他の答えは機能しますが、__rdtscを含めることで利用できるGCCのx86intrin.h組み込み関数を使用して、インラインアセンブリを回避できます。

次のように定義されています: gcc/config/i386/ia32intrin.h

/* rdtsc */
extern __inline unsigned long long
__attribute__((__gnu_inline__, __always_inline__, __artificial__))
__rdtsc (void)
{
  return __builtin_ia32_rdtsc ();
}
28
Evan Shaw

更新:この回答を再投稿して更新より標準的な質問。同様のrdtsc質問をすべて閉じるための重複ターゲットとして使用する質問を整理したら、おそらくいつかこれを削除します。


これにはインラインasmを使用する必要はなく、使用しないでください。メリットはありません。コンパイラにはrdtscrdtscpの組み込みがあり、(少なくとも最近では)適切なヘッダーを含めると、すべて___rdtsc_組み込み関数が定義されます。 https://gcc.gnu.org/wiki/DontUseInlineAsm

残念ながら、MSVCは、SIMD以外の組み込み関数に使用するヘッダーについて他のすべての人と意見が一致していません。 ( Intelの組み込みガイドによると _#include <immintrin.h>_ですが、gccとclangを使用すると、非SIMD組み込み関数はほとんど_x86intrin.h_にあります。)

_#ifdef _MSC_VER
#include <intrin.h>
#else
#include <x86intrin.h>
#endif

// optional wrapper if you don't want to just use __rdtsc() everywhere
inline
unsigned long long readTSC() {
    // _mm_lfence();  // optionally wait for earlier insns to retire before reading the clock
    return __rdtsc();
    // _mm_lfence();  // optionally block later instructions until rdtsc retires
}
_

4つの主要なコンパイラすべてでコンパイルします:gcc/clang/ICC/MSVC、32ビットまたは64ビット用。の結果Godboltコンパイラエクスプローラ

lfenceを使用してrdtscの再現性を向上させる方法の詳細については、 clflushを使用してC関数を介してキャッシュラインを無効にする に関する@HadiBraisの回答を参照してください。

参照 AMDプロセッサでLFENCEがシリアル化されていますか? (TL:DRはい、Spectre軽減が有効になっています。そうでない場合、カーネルは関連するMSRを未設定のままにします。)


rdtscカウント参照サイクル、CPUコアクロックサイクルではありません

ターボ/省電力に関係なく固定周波数でカウントされるため、1クロックあたりのuops分析が必要な場合は、パフォーマンスカウンターを使用してください。 rdtscは実時間と正確に相関しています(システムクロックの調整を除いて、基本的には_steady_clock_です)。 CPUの定格周波数、つまりアドバタイズされたステッカー周波数で刻みます。

マイクロベンチマークに使用する場合は、最初にウォームアップ期間を含めて、タイミングを開始する前にCPUがすでに最大クロック速度になっていることを確認してください。または、ハードウェアパフォーマンスカウンターへのアクセスを提供するライブラリを使用するか、 プログラムの一部のperf stat のようなトリックを使用します(時間指定された領域が_perf stat -p PID_をアタッチできるほど長い場合)。ただし、通常は、マイクロベンチマーク中のCPU周波数シフトを回避する必要があります。

すべてのコアのTSCが同期していることも保証されていません。したがって、スレッドが__rdtsc()の間に別のCPUコアに移行する場合、余分なスキューが発生する可能性があります。 (ただし、ほとんどのOSはすべてのコアのTSCを同期しようとします。)rdtscを直接使用している場合は、プログラムまたはスレッドをコアに固定する必要があります。 Linuxでは_taskset -c 0 ./myprogram_を使用します。


Asmは組み込みを使用することでどれほど優れていますか?

少なくともインラインasmでできることと同じくらい良いです。

それの非インラインバージョンは、次のようにx86-64用のMSVCをコンパイルします。

_unsigned __int64 readTSC(void) PROC                             ; readTSC
    rdtsc
    shl     rdx, 32                             ; 00000020H
    or      rax, rdx
    ret     0
  ; return in RAX
_

_edx:eax_で64ビット整数を返す32ビットの呼び出し規約の場合、それはrdtsc/retだけです。それは重要ではありません、あなたは常にこれをインラインにしたいです。

それを2回使用し、間隔を時間に差し引くテスト呼び出し元では、次のようになります。

_uint64_t time_something() {
    uint64_t start = readTSC();
    // even when empty, back-to-back __rdtsc() don't optimize away
    return readTSC() - start;
}
_

4つのコンパイラはすべて、非常によく似たコードを作成します。これはGCCの32ビット出力です。

_# gcc8.2 -O3 -m32
time_something():
    Push    ebx               # save a call-preserved reg: 32-bit only has 3 scratch regs
    rdtsc
    mov     ecx, eax
    mov     ebx, edx          # start in ebx:ecx
      # timed region (empty)

    rdtsc
    sub     eax, ecx
    sbb     edx, ebx          # edx:eax -= ebx:ecx

    pop     ebx
    ret                       # return value in edx:eax
_

これはMSVCのx86-64出力です(名前修飾が適用されています)。 gcc/clang/ICCはすべて同じコードを出力します。

_# MSVC 19  2017  -Ox
unsigned __int64 time_something(void) PROC                            ; time_something
    rdtsc
    shl     rdx, 32                  ; high <<= 32
    or      rax, rdx
    mov     rcx, rax                 ; missed optimization: lea rcx, [rdx+rax]
                                     ; rcx = start
     ;; timed region (empty)

    rdtsc
    shl     rdx, 32
    or      rax, rdx                 ; rax = end

    sub     rax, rcx                 ; end -= start
    ret     0
unsigned __int64 time_something(void) ENDP                            ; time_something
_

4つのコンパイラはすべて、orの代わりにmov + leaを使用して、下位半分と上位半分を異なるレジスタに結合します。彼らが最適化に失敗するのは一種の缶詰のシーケンスだと思います。

しかし、インラインasmでそれを自分で書くことはほとんど良いことではありません。 32ビットの結果のみを保持するような短い間隔のタイミングをとっている場合は、EDXの結果の上位32ビットを無視する機会をコンパイラーから奪うことになります。または、コンパイラが開始時刻をメモリに格納することを決定した場合、shift/or/movの代わりに2つの32ビットストアを使用することができます。タイミングの一部として1つの余分なuopが気になる場合は、マイクロベンチマーク全体を純粋なasmで記述したほうがよいでしょう。

12
Peter Cordes

gccを使用するLinuxでは、次を使用します。

/* define this somewhere */
#ifdef __i386
__inline__ uint64_t rdtsc() {
  uint64_t x;
  __asm__ volatile ("rdtsc" : "=A" (x));
  return x;
}
#Elif __AMD64
__inline__ uint64_t rdtsc() {
  uint64_t a, d;
  __asm__ volatile ("rdtsc" : "=a" (a), "=d" (d));
  return (d<<32) | a;
}
#endif

/* now, in your function, do the following */
uint64_t t;
t = rdtsc();
// ... the stuff that you want to time ...
t = rdtsc() - t;
// t now contains the number of cycles elapsed
5
a3nm