获取CPU周期数?
我在SO上看到这篇包含C代码的post来获取最新的CPU周期数:
基于CPU周期计算的C / C ++ Linux x86_64中的分析
有没有办法在C ++中使用这段代码(欢迎使用windows和linux解决方案)? 虽然用C语言编写(而C是C ++的一个子集)但我不太确定这段代码是否适用于C ++项目,如果没有,如何翻译呢?
我使用的是x86-64
EDIT2:
找到此function但无法让VS2010识别汇编程序。 我需要包含任何内容吗? (我相信我必须将uint64_t
交换为多long long
的窗口……?)
static inline uint64_t get_cycles() { uint64_t t; __asm volatile ("rdtsc" : "=A"(t)); return t; }
EDIT3:
从上面的代码我得到错误:
“错误C2400:’操作码’中的内联汇编语法错误;找到’数据类型’”
有人可以帮忙吗?
从GCC 4.5及更高版本开始,MSVC和GCC现在都支持__rdtsc()
内在函数。
但是需要的包含是不同的:
#ifdef _WIN32 #include #else #include #endif
这是GCC 4.5之前的原始答案。
直接拉出我的一个项目:
#include // Windows #ifdef _WIN32 #include uint64_t rdtsc(){ return __rdtsc(); } // Linux/GCC #else uint64_t rdtsc(){ unsigned int lo,hi; __asm__ __volatile__ ("rdtsc" : "=a" (lo), "=d" (hi)); return ((uint64_t)hi << 32) | lo; } #endif
VC ++使用完全不同的语法进行内联汇编 – 但仅限于32位版本。 64位编译器根本不支持内联汇编。
在这种情况下,这可能也是一样 – 在定时代码序列方面, rdtsc
(至少)有两个主要问题。 首先(像大多数指令一样)它可以不按顺序执行,所以如果你试图计算一小段代码,那么rdtsc
之前和之后的rdtsc
可能都会在它之前执行,或者在它之后执行,或者什么都有你(我相当肯定这两个人总是按照彼此的顺序执行,所以至少差别永远不会是负面的)。
其次,在多核(或多处理器)系统上,一个rdtsc可以在一个核/处理器上执行,另一个在不同的核/处理器上执行。 在这种情况下,完全可能产生否定结果。
一般来说,如果你想在Windows下使用精确的计时器,那么使用QueryPerformanceCounter
会更好。
如果你真的坚持使用rdtsc
,我相信你必须在一个完全用汇编语言编写的单独模块中(或使用编译器内部函数),然后用你的C或C ++链接。 我从来没有为64位模式编写代码,但在32位模式下,它看起来像这样:
xor eax, eax cpuid xor eax, eax cpuid xor eax, eax cpuid rdtsc ; save eax, edx ; code you're going to time goes here xor eax, eax cpuid rdtsc
我知道这看起来很奇怪,但它确实是对的。 您执行CPUID是因为它是一个序列化指令(不能无序执行),并且在用户模式下可用。 你在开始计时之前执行了三次,因为英特尔记录了第一次执行可以/将以不同于第二次执行的速度执行的事实(他们推荐的是三次,所以三次执行)。
然后你执行你的代码测试,另一个cpuid强制序列化,最后的rdtsc在代码完成后得到时间。
除此之外,您还希望使用操作系统提供的任何方法来强制这一切在一个进程/核心上运行。 在大多数情况下,您还希望强制执行代码对齐 – 对齐方式的更改可能会导致执行语言的相当大的差异。
最后你想多次执行它 – 它总是有可能在事物中间被中断(例如,一个任务切换),所以你需要做好准备,以便执行相当长的时间。比其余的更长 – 例如,5次运行每个需要大约40-43个时钟周期,第六次运行需要10000多个时钟周期。 显然,在后一种情况下,你只是抛弃exception值 – 它不是来自你的代码。
总结:管理执行rdtsc指令本身(几乎)是您最不担心的问题。 在你从rdtsc
获得实际上意味着任何东西的结果之前,你还需要做更多的事情。
你不需要内联asm 。 没有好处; 编译器有rdtsc
和rdtscp
内置rdtscp
,并且(至少这些天)如果包含正确的头文件,则所有都定义__rdtsc
内在函数。 但与几乎所有其他情况( https://gcc.gnu.org/wiki/DontUseInlineAsm )不同,asm并没有严重的缺点, 只要你使用像@ Mysticial这样的好的和安全的实现 ,而不是一个破坏的"=A"
约束 。
不幸的是,MSVC不同意其他人关于哪些标头用于非SIMD内在函数。
英特尔的ininiscs指南说_rdtsc
(带有一个下划线)在
,但这对gcc和clang不起作用。 他们只在
定义了SIMD内在函数,因此我们坚持使用
(MSVC)与
(其他所有内容,包括最近的ICC)。 为了兼容MSVC和英特尔的文档,gcc和clang定义了函数的单下划线和双下划线版本。
有趣的事实:双下划线版本返回无符号的64位整数,而英特尔_rdtsc()
作为返回(签名) _rdtsc()
。
// valid C99 and C++ #include // is preferred in C++, but stdint.h works. #ifdef _MSC_VER # include #else # include #endif // optional wrapper if you don't want to just use __rdtsc() everywhere inline uint64_t readTSC() { // _mm_lfence(); // optionally wait for earlier insns to retire before reading the clock uint64_t tsc = __rdtsc(); // _mm_lfence(); // optionally block later instructions until rdtsc retires return tsc; } // requires a Nehalem or newer CPU. Not Core2 or earlier. IDK when AMD added it. inline uint64_t readTSCp() { unsigned dummy; return __rdtscp(&dummy); // waits for earlier insns to retire, but allows later to start }
编译所有4个主要编译器:gcc / clang / ICC / MSVC,32或64位。 查看Godbolt编译器资源管理器上的结果 ,包括几个测试调用者。
这些内在函数是gcc4.5(2010年起)和clang3.5(2014年起)的新内容 。 关于Godbolt的gcc4.4和clang 3.4不编译这个,但gcc4。5。3(2011年4月)确实如此。 您可能会在旧代码中看到内联asm,但您可以并且应该使用__rdtsc()
替换它。 十多年前的编译器通常比gcc6,gcc7或gcc8编写更慢的代码,并且具有较少的有用错误消息。
MSVC内在函数(我认为)存在的时间要长得多,因为MSVC从不支持x86-64的内联asm。 ICC13在__rdtsc
中有immintrin.h
,但根本没有x86intrin.h
。 最近的ICC有x86intrin.h
,至少是Godbolt为Linux安装它们的方式。
您可能希望将它们定义为long long
,特别是如果要将它们减去并转换为float。 int64_t
– > float / double比没有AVX512的x86上的uint64_t
更有效。 此外,如果TSC没有完全同步,那么由于CPU迁移可能会产生小的负面结果,这可能比大的无符号数更有意义。
BTW,clang还有一个可移植的__builtin_readcyclecounter()
,适用于任何架构。 (对于没有循环计数器的体系结构,始终返回零。)请参阅clang / LLVM语言扩展文档
有关使用lfence
(或cpuid
)来提高rdtsc
可重复性并通过阻止无序执行来确切控制哪些指令在定时间隔内的更多信息 ,请参阅@HadiBrais对clflush的回答以通过C使高速缓存行无效函数和注释作为它所产生的差异的一个例子。
另请参阅AMD处理器上的LFENCE序列化吗? (TL:DR是启用了Specter缓解,否则内核会保留相关的MSR,因此您应该使用cpuid
进行序列化。)它始终被定义为在Intel上进行部分序列化。
如何在英特尔®IA-32和IA-64指令集架构上对代码执行时间进行基准测试 ,这是2010年的英特尔白皮书。
rdtsc
计算参考周期,而不是CPU核心时钟周期
无论涡轮/省电,它都以固定频率计数,因此如果您需要每时钟uops分析,请使用性能计数器。 rdtsc
与挂钟时间完全相关(系统时钟调整除外,因此它是stable_clock的完美时间steady_clock
)。 它以CPU的额定频率(即广告标签频率)为准。 (或者几乎就是这样。例如i7-6700HQ 2.6 GHz Skylake上的2592 MHz。)
如果将其用于微基准测试,请首先包括预热时间段,以确保在开始计时之前CPU已处于最大时钟速度。 (并且可选择禁用turbo并告诉操作系统更喜欢最大时钟速度,以避免在微基准测试期间CPU频率偏移)。 或者更好的是,使用可以访问硬件性能计数器的库,或者如果您的定时区域足够长以便可以附加perf stat -p PID
,则可以使用诸如程序部分的perf stat之类的技巧。
但是,你通常仍然希望为微基准测试保持CPU时钟的固定,除非你想看看不同的负载如何让Skylake在内存受限或其他情况下降低时钟。 (请注意,内存带宽/延迟大多是固定的,使用与内核不同的时钟。在空闲时钟速度下,L2或L3缓存未命中会占用更少的内核时钟周期。)
- 使用背靠背rdtsc进行负时钟周期测量? RDTSC的历史:原来CPU没有省电,所以TSC既是实时也是核心时钟。 然后,它通过各种几乎没用的步骤演变成当前forms的有用的低开销时间源,与核心时钟周期(
constant_tsc
)分离,当时钟停止(nonstop_tsc
)时不会停止。 还有一些提示,例如不采取平均时间,取中位数(将有非常高的exception值)。 - std :: chrono ::时钟,硬件时钟和周期数
- 使用RDTSC获取cpu周期 – 为什么RDTSC的值总是增加?
- 英特尔失去了周期? rdtsc和CPU_CLK_UNHALTED.REF_TSC之间的不一致
- 使用RDTSC指令测量C中的代码执行时间列出了一些问题,包括即使在具有
cli
内核模式下也无法避免的SMI(系统管理中断),以及VM下的rdtsc
虚拟化。 当然,基本的东西,如常规中断是可能的,所以重复你的时间很多次,扔掉exception值。 -
确定Linux上的TSC频率 。 以编程方式查询TSC频率很难并且可能不可能,尤其是在用户空间中,或者可能比校准它更糟糕 。 使用另一个已知的时间源来校准它需要时间。 请参阅该问题,了解更多关于将TSC转换为纳秒的难度(如果您可以询问操作系统的转换率是多少,那将会很好,因为操作系统已经在启动时执行了此操作)。
如果您使用RDTSC进行微基准测试以进行调整,那么最好的办法就是使用刻度并跳过甚至尝试转换为纳秒。 否则,使用高分辨率库时间函数,如
std::chrono
或clock_gettime
。 对于时间戳函数的某些讨论/比较,或者从内存中读取共享时间戳,以查看更快的等效gettimeofday ,如果您的精度要求足够低,以便定时器中断或线程更新,则可以完全避免rdtsc
。另请参阅使用rdtsc计算系统时间,了解有关查找晶体频率和乘数的信息。
它也不能保证所有核心的TSC同步 。 因此,如果您的线程迁移到__rdtsc()
之间的另一个CPU核心,则可能会有额外的偏差。 (大多数操作系统尝试同步所有内核的TSC,所以通常它们会非常接近。)如果你直接使用rdtsc
,你可能想把你的程序或线程固定到核心,例如使用taskset -c 0 ./myprogram
Linux上的taskset -c 0 ./myprogram
。
特别是在多核多处理器环境中的CPU TSC获取操作表明, Nehalem和更新版本的TSC已同步并锁定在一个包中的所有核心 (即不变TSC)。 但多插槽系统仍然是一个问题。 即使是较旧的系统(如2007年的Core2之前)也可能有一个TSC在核心时钟停止时停止,或者与实际核心时钟频率而不是参考周期相关联。 (较新的CPU总是具有恒定的TSC和不间断的TSC。)有关更多详细信息,请参阅@ amdn关于该问题的答案。
使用内在的asm有多好?
它与@ Mysticial的GNU C inline asm一样好,或者更好,因为它知道RAX的高位是零。 你想要保持内联asm的主要原因是为了与硬件旧编译器进行比较。
readTSC
函数本身的非内联版本与MSVC for x86-64一起编译,如下所示:
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装序列。
但是在内联中自行编写一个shift / lea并不是更好。 如果你计时这么短的间隔,你只能保留32位的结果,你就会剥夺编译器在EDX中忽略结果的高32位的机会。 或者如果编译器决定将开始时间存储到内存中,它可能只使用两个32位存储而不是shift /或/ mov。 如果一个额外的uop作为你的时间的一部分困扰你,你最好用纯粹的asm编写你的整个微基准。
但是,我们可以通过修改后的@ Mysticial代码获得两全其美:
// More efficient than __rdtsc() in some case, but maybe worse in others uint64_t rdtsc(){ // long and uintptr_t are 32-bit on the x32 ABI (32-bit pointers in 64-bit mode), so #ifdef would be better if we care about this trick there. unsigned long lo,hi; // let the compiler know that zero-extension to 64 bits isn't required __asm__ __volatile__ ("rdtsc" : "=a" (lo), "=d" (hi)); return ((uint64_t)hi << 32) + lo; // + allows LEA or ADD instead of OR }
在Godbolt上 ,这确实比gcc / clang / ICC的__rdtsc()
更好的asm,但有时它会欺骗编译器使用额外的寄存器来单独保存lo和hi,所以clang可以优化成((end_hi-start_hi)<<32) + (end_lo-start_lo)
。 希望如果有真正的注册压力,编译器将会更早结合。 (gcc和ICC仍然分别保存lo / hi,但也没有优化。)
但是32位gcc8搞得一团糟,甚至只用rdtsc()
函数本身编译一个带零的实际add/adc
,而不是只返回edx中的结果:eax就像clang一样。 (gcc6和更早版本用|
代替+
,但如果你关心来自gcc的32位代码,肯定更喜欢__rdtsc()
内在函数)。
对于Windows,Visual Studio提供了一个方便的“编译器内在”(即编译器可以理解的特殊函数),它为您执行RDTSC指令并返回结果:
unsigned __int64 __rdtsc(void);