如何在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 

在最新版本的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 

其他答案有效,但您可以通过使用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 (); } 

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

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


您不需要也不应该使用内联asm 。 没有好处; 编译器有rdtscrdtscp内置rdtscp ,并且(至少这些天)如果包含正确的头文件,则所有都定义__rdtsc内在函数。 https://gcc.gnu.org/wiki/DontUseInlineAsm

不幸的是,MSVC不同意其他人关于哪些标头用于非SIMD内在函数。 ( 英特尔的intriniscs指南为此说 #include ,但是对于gcc和clang,非SIMD内在函数主要在x86intrin.h 。)

 #ifdef _MSC_VER #include  #else #include  #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提高lfence重复性的rdtsc ,请参阅@HadiBrais对clflush的回答, 通过C函数使缓存行无效 。

另请参阅AMD处理器上的LFENCE序列化吗? (TL:DR是启用了Spectre缓解,否则内核将保留相关的MSR。)


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

无论涡轮/省电,它都以固定频率计数,因此如果您需要每时钟uops分析,请使用性能计数器。 rdtsc与挂钟时间完全相关(系统时钟调整除外,所以它基本上是steady_clock )。 它以CPU的额定频率(即广告标签频率)为准。

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

  • std :: chrono ::时钟,硬件时钟和周期数
  • 使用RDTSC获取cpu周期 – 为什么RDTSC的值总是增加?
  • 英特尔失去了周期? rdtsc和CPU_CLK_UNHALTED.REF_TSC之间的不一致

它也不能保证所有核心的TSC同步 。 因此,如果您的线程迁移到__rdtsc()之间的另一个CPU核心,则可能会有额外的偏差。 (但是,大多数操作系统都会尝试同步所有内核的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 。 并不重要,你总是希望这个内联。

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

 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将低半部分和高半部分组合成不同的寄存器。 我猜这是一种他们未能优化的jar装序列。

但是自己写入内联并不是更好。 如果你计时这么短的间隔,你只能保留32位的结果,你就会剥夺编译器在EDX中忽略结果的高32位的机会。 或者如果编译器决定将开始时间存储到内存中,它可能只使用两个32位存储而不是shift /或/ mov。 如果一个额外的uop作为你的时间的一部分困扰你,你最好用纯粹的asm编写你的整个微基准。