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

Joh*_*åde 15 c c++ x86 gcc 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
Run Code Online (Sandbox Code Playgroud)

Eva*_*haw 26

其他答案可行,但您可以通过使用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 ();
}
Run Code Online (Sandbox Code Playgroud)


And*_*zos 24

在最新版本的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
Run Code Online (Sandbox Code Playgroud)

  • 是的,我确实需要 RDTSC,现在我拥有了。谢谢。 (2认同)
  • 你根本不需要内联汇编。我使用在所有 4 个主要 x86 编译器上编译的 `__rdtsc()` 添加了一个现代答案。 (2认同)

Pet*_*des 12

更新:在一个更规范的问题上重新发布并更新了此答案。一旦我们选出哪个问题作为关闭所有类似rdtsc问题的重复目标,我可能会在某个时候删除它。


您不需要,也不应该为此使用嵌入式asm。没有好处。编译器已经内置插件的rdtscrdtscp,和(至少这些天)都定义了一个__rdtsc内在的,如果你有正确的头。 https://gcc.gnu.org/wiki/DontUseInlineAsm

不幸的是,MSVC与其他所有人不同意在非SIMD内部函数使用哪个标头。(英特尔的内部指南 #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
}
Run Code Online (Sandbox Code Playgroud)

可使用所有4种主要编译器进行编译:gcc / clang / ICC / MSVC,适用于32位或64位。在Godbolt编译器资源管理器上 查看结果

有关lfence用于提高重复性的更多信息rdtsc,请参见@HadiBrais在clflush上的回答,以通过C函数使缓存行无效

另请参阅LFENCE是否在AMD处理器上序列化?(启用Spectre缓解功能的TL:DR是,否则内核未设置相关的MSR。)


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

无论涡轮增压或节能模式如何,它都以固定频率计数,因此,如果您希望每时钟分析一次,请使用性能计数器。 rdtsc与壁钟时间完全相关(系统时钟调整除外,因此基本上是steady_clock)。它在CPU的额定频率(即广告的标贴频率)上滴答作响。

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

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


使用内部函数对asm有多好?

至少与使用内联汇编可以执行的任何操作一样好。

它的非嵌入式版本可以像这样编译用于x86-64的MSVC:

unsigned __int64 readTSC(void) PROC                             ; readTSC
    rdtsc
    shl     rdx, 32                             ; 00000020H
    or      rax, rdx
    ret     0
  ; return in RAX
Run Code Online (Sandbox Code Playgroud)

对于在其中返回64位整数的32位调用约定edx:eax,它只是rdtsc/ ret。没关系,您总是希望内联。

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

uint64_t time_something() {
    uint64_t start = readTSC();
    // even when empty, back-to-back __rdtsc() don't optimize away
    return readTSC() - start;
}
Run Code Online (Sandbox Code Playgroud)

所有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
Run Code Online (Sandbox Code Playgroud)

这是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
Run Code Online (Sandbox Code Playgroud)

所有4个编译器都使用or+ mov而不是lea将低半部分和高半部分组合到不同的寄存器中。我想这是他们无法优化的固定顺序。

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

  • 虽然我总体上同意 DontUseInlineAsm 建议,但似乎调用“rdtsc”(只是一条指令,具有适当的输入和输出依赖项:似乎它将解决“忽略 edx 问题”)几乎是这样的情况它永远不会成为问题。我主要是因为 `x86intrin.h` 是一个巨大的标头,在我的系统上解析需要 300 毫秒。 (2认同)

a3n*_*3nm 5

在Linux上gcc,我使用以下内容:

/* 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
Run Code Online (Sandbox Code Playgroud)