如何在GCC x86中使用RDTSC计算时钟周期?

时间:2012-03-27 10:32:39

标签: c++ c 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

4 个答案:

答案 0 :(得分:25)

其他答案有效,但您可以使用GCC的__rdtsc内在函数来避免内联汇编,包括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 ();
}

答案 1 :(得分:21)

在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

答案 2 :(得分:6)

更新: reposted and updated this answer 涉及一个更规范的问题。一旦我们找出要关闭所有相似的rdtsc问题的重复目标,我可能会在某个时候删除它。


您不需要,也不应该为此使用嵌入式asm 。没有好处。编译器具有rdtscrdtscp的内置程序,并且(至少最近这些天)都定义了__rdtsc内在函数(如果您包括正确的头文件)。 https://gcc.gnu.org/wiki/DontUseInlineAsm

不幸的是,MSVC与其他所有人不同意在非SIMD内部函数使用哪个标头。 (为此,Intel's intriniscs guide says #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位。参见the results on the Godbolt compiler explorer

有关使用lfence来提高rdtsc的可重复性的更多信息,请参见@HadiBrais对clflush to invalidate cache line via C function的回答。

另请参见Is LFENCE serializing on AMD processors?(启用Spectre缓解功能的TL:DR是,否则内核未设置相关的MSR。)


rdtsc计算的是参考周期,而不是CPU核心时钟周期

无论turbo /节能模式如何,它都以固定的频率进行计数,因此,如果要进行每时钟一次的分析,请使用性能计数器。 rdtsc与挂钟时间完全相关(系统时钟调整除外,因此基本上是steady_clock)。它以CPU的额定频率(即广告的标贴频率)为刻度。

如果将其用于微基准测试,请在开始计时之前先包括一个预热时间,以确保您的CPU已经达到最大时钟速度。或更妙的是,使用可以访问硬件性能计数器的库,或者使用perf stat for part of program之类的技巧,如果您的定时区域足够长以至于您可以附加perf stat -p PID。不过,您通常仍然希望避免在微基准测试期间发生CPU频率偏移。

也不能保证所有内核的TSC都是同步的。因此,如果您的线程在__rdtsc()之间迁移到另一个CPU内核,则可能会有额外的偏差。 (不过,大多数操作系统都尝试同步所有内核的TSC。)如果您直接使用rdtsc,则可能希望将程序或线程固定到某个内核,例如在Linux上使用taskset -c 0 ./myprogram


使用内部函数对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。没关系,您总是希望它可以内联。

在两次使用它并减去时间间隔的测试调用方中:

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将上下半部分组合到一个不同的寄存器中。我想这是他们无法优化的固定顺序。

但是您自己以内联汇编编写它几乎没有更好的选择。如果您安排的时间间隔太短,只能保留32位结果,那么您将剥夺编译器去忽略EDX中高32位结果的机会。或者,如果编译器决定将开始时间存储到内存中,则可以只使用两个32位存储而不是shift / or / mov。如果您在计时过程中多了1个UOP,那么您最好用纯asm编写整个微基准测试。

答案 3 :(得分:5)

在使用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