如何从C++中获取x86_64的CPU周期计数?

60
我在SO上看到了这篇文章,其中包含获取最新CPU周期计数的C代码: 基于C/C++ Linux x86_64的CPU周期计数性能分析 是否有办法在C++中使用此代码(欢迎提供Windows和Linux解决方案)?虽然该代码是用C编写的(而C是C++的子集),但我不太确定它是否适用于C++项目,如果不行,应如何翻译?
我正在使用x86-64。
编辑2:
找到了这个函数,但无法让VS2010识别汇编程序。我需要包含什么吗?(我相信我必须将uint64_t换成long long以适用于Windows....?)
static inline uint64_t get_cycles()
{
  uint64_t t;
  __asm volatile ("rdtsc" : "=A"(t));
  return t;
}

编辑3:

从上面的代码中,我遇到了以下错误:

"error C2400: inline assembler syntax error in 'opcode'; found 'data type'"

请问有人可以帮忙吗?


1
Visual Studio不支持在x86-64上进行汇编。 - Mark Ransom
1
要获取 uint64_t,您应该#include <stdint.h>(实际上是<cstdint>,但您的编译器可能太旧无法支持它)。 - Nikos C.
@user997112,是的,我指的是MSVC。我完全忘记了你可以在其中替换编译器,因为我从未尝试过。 - Mark Ransom
伙计们,我现在在edit3中遇到了错误。我已经包含了<stdint.h>头文件,并且这是在Windows 7系统上。 - user997112
你需要小心处理这个问题。在多核芯片上,不同核心的时钟计数是不同的。如果调度程序在不同核心之间移动你的线程,计数可能会跳跃。一些操作系统已经解决了这个问题。一些芯片会将核心置于睡眠状态以节省电力,此时该核心的时钟不会前进。 - brian beuning
显示剩余3条评论
5个回答

84

从GCC 4.5及更高版本开始,__rdtsc()内置函数现在由MSVC和GCC都支持。

但是所需的include不同:

#ifdef _WIN32
#include <intrin.h>
#else
#include <x86intrin.h>
#endif

以下是 GCC 4.5 之前的原始答案。

直接从我的一个项目中提取出来:

#include <stdint.h>

//  Windows
#ifdef _WIN32

#include <intrin.h>
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

这个 GNU C Extended asm 告诉编译器:
  • volatile:输出不是输入的纯函数(因此每次都必须重新运行,而不能重复使用旧结果)。
  • "=a"(lo)"=d"(hi):输出操作数是固定寄存器:EAX和EDX。(x86机器约束)。x86 rdtsc 指令将其64位结果放在EDX:EAX中,因此让编译器选择"=r"的输出不起作用:没有办法要求CPU将结果放在其他地方。
  • ((uint64_t)hi << 32) | lo - 将两个32位半部分零扩展为64位(因为lo和hi是unsigned),然后逻辑移位+ OR它们到一个单独的64位C变量中。在32位代码中,这只是一种重新解释的方式;值仍然只停留在一对32位寄存器中。在64位代码中,您通常会得到一个实际的移位+ OR汇编指令,除非高半部分被优化掉。

(编辑注:如果您使用unsigned long而不是unsigned int,则可能会更有效率。然后编译器将知道lo已经被零扩展为RAX。它不会知道上半部分是否为零,因此|+在合并不同方式时是等效的。理论上,内在的应该可以让您在让优化器做好工作方面得到最佳效果。)

https://gcc.gnu.org/wiki/DontUseInlineAsm如果可以避免,请勿使用内联汇编。但是,如果您需要理解使用内联汇编的旧代码以便使用intrinsic重写它,则希望本节内容有用。另请参见https://stackoverflow.com/tags/inline-assembly/info


这是一种很好的打包方式。 - Nik Bougalis
9
值得一提的是,gcc 4.5及更高版本已经包含了__rdtsc()函数——需要包含<x86intrin.h>头文件。该头文件还包括了Microsoft的<intrin.h>中发现的许多其他Intel内部函数,并且在包含大多数SIMD头文件(如emmintrin.h、xmmintrin.h等)时,它会自动被包含进来。 - jstine
2
@Orient:仅在32位模式下。在64位模式下,"=A"将选择RAX或RDX中的任意一个 - Peter Cordes
1
你为什么喜欢在GNU编译器中使用内联汇编? <x86intrin.h> 为除MSVC以外的编译器定义了 __rdtsc(),因此您可以只需添加 #ifdef _MSC_VER。我在这个问题上添加了一个答案,因为它看起来是关于 rdtsc 内部函数和如何使用 rdtsc 的陷阱的一个很好的地方。 - Peter Cordes
@PeterCordes 请看jstine的评论。在当时,rdtsc内置函数并不存在。 - Mysticial
显示剩余4条评论

68
你的x86-64的内联汇编有问题。在64位模式下,"=A"让编译器选择RAX或RDX中的一个,而不是EDX:EAX。请参考这个问答了解更多信息。
你不需要使用内联汇编。这没有任何好处;编译器已经内置了rdtscrdtscp的功能,并且(至少现在)都定义了__rdtsc的内部函数,只要你包含了正确的头文件。但与几乎所有其他情况(https://gcc.gnu.org/wiki/DontUseInlineAsm)不同的是,使用汇编没有严重的缺点,只要你使用像@Mysticial的好而安全的实现

(汇编的一个小优势是,如果你想计时一个肯定不会超过2^32个计数的小时间间隔,你可以忽略结果的高半部分。编译器可以通过uint32_t time_low = __rdtsc()的内部函数来进行这种优化,但实际上它们有时仍然会浪费指令进行移位/或运算。)


很不幸,MSVC与其他人对于非SIMD内嵌函数所使用的头文件存在分歧。
Intel的内嵌函数指南中提到,使用一个下划线的"_rdtsc"应该在""中,但是这在gcc和clang上并不起作用。它们只在""中定义了SIMD内嵌函数,所以我们只能选择使用""(MSVC)或者""(其他所有编译器,包括最近的ICC)。为了与MSVC和Intel的文档兼容,gcc和clang同时定义了带一个下划线和两个下划线的函数版本。
有趣的事实:双下划线版本返回一个无符号的64位整数,而英特尔文档中将_rdtsc()返回为(有符号的)__int64
// valid C99 and C++

#include <stdint.h>  // <cstdint> is preferred in C++, but stdint.h works.

#ifdef _MSC_VER
# include <intrin.h>
#else
# include <x86intrin.h>
#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;
    uint64_t tsc = __rdtscp(&dummy); // waits for earlier insns to retire
    // _mm_lfence();  // optionally block later instructions until rdtscp retires
    // in practice most CPUs seem to implement rdtscp similar to lfence; rdtsc, so later instructions won't start while waiting for earlier to finish.  But not guaranteed on paper.
    // see also the section below about out of order exec.
    return tsc;  
}

支持所有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月)可以。你可能会在旧代码中看到内联汇编,但你可以并且应该用`__rdtsc()`来替换它。十多年前的编译器通常会生成比gcc6、gcc7或gcc8更慢的代码,并且错误信息也不太有用。
我认为,MSVC内置函数存在的时间更长,因为MSVC从未支持过x86-64的内联汇编。ICC13在`immintrin.h`中有`__rdtsc`,但根本没有`x86intrin.h`。更近期的ICC有`x86intrin.h`,至少在Godbolt为Linux安装的方式中是这样的。
你可能想将它们定义为有符号的"long long",特别是如果你想要进行减法运算并转换为浮点数。在x86架构上,将"int64_t"转换为float/double比将"uint64_t"转换为float/double更高效,除非使用了AVX512指令集。此外,如果时间戳计数器(TSC)没有完全同步,由于CPU迁移,可能会出现较小的负数结果,这可能比巨大的无符号数更合理。
顺便说一句,clang还有一个便携的__builtin_readcyclecounter(),适用于任何架构。(在没有循环计数器的架构上始终返回零。)请参阅{{link1:clang/LLVM语言扩展文档}}。
有关使用lfence(或cpuid)来提高rdtsc的可重复性,并通过阻止乱序执行来控制确切的指令在计时间隔内/外的更多信息,请参阅HadiBrais在clflush to invalidate cache line via C function的回答以及评论,这将展示其所带来的差异。还有solution to rdtsc out of order execution?
另请参阅Is LFENCE serializing on AMD processors?(简而言之,启用Spectre缓解时是的,否则内核会将相关的MSR未设置,因此应使用cpuid进行序列化)。在Intel上,它一直被定义为部分序列化。
《如何在Intel® IA-32和IA-64指令集架构上对代码执行时间进行基准测试》是一篇来自2010年的英特尔白皮书。但请注意,它建议使用cpuid来序列化执行通常是一个不好的建议;它很慢,在虚拟机上更是非常慢,因为它会导致vmexit。如果你想等待之前的存储提交,可以使用mfence; lfence; rdtsc,否则只需使用lfence等待ROB的退休即可。

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

它以固定频率计数,不受Turbo / 节能模式的影响,因此如果您想进行每个时钟周期的uops分析,请使用性能计数器。 rdtsc与挂钟时间完全相关(不包括系统时钟调整),因此它是steady_clock的完美时间源。

TSC频率曾经总是等于CPU的额定频率,即宣传的贴纸频率。在某些CPU上,它只是接近,例如i7-6700HQ 2.6 GHz Skylake上的2592 MHz,或者i7-6700k 4000MHz上的4008MHz。在像i5-1035 Ice Lake这样的更新CPU上,TSC = 1.5 GHz,基频 = 1.1 GHz,因此禁用Turbo对于这些CPU上的TSC = 核心周期甚至近似无效。

如果你用它来进行微基准测试,请先包含一个热身阶段,确保你的CPU已经达到最大时钟速度,然后再开始计时。(可选择禁用超频,并告诉操作系统优先选择最大时钟速度,以避免微基准测试期间的CPU频率变化)。微基准测试很难:参考Idiomatic way of performance evaluation?了解其他注意事项。
你可以使用一个提供硬件性能计数器访问权限的库,而不是完全依赖TSC。复杂但开销较低的方法是在用户空间编程perf计数器并使用rdmsr,或者更简单的方法是使用类似perf stat for part of program的技巧,如果你的计时区域足够长,可以附加一个perf stat -p PID命令。
通常情况下,对于微基准测试,您通常仍然希望保持CPU时钟固定,除非您想看到在受内存限制或其他情况下,不同负载会如何导致Skylake降频。(请注意,内存带宽/延迟大部分是固定的,使用与核心不同的时钟。在空闲时钟速度下,L2或L3缓存未命中所需的核心时钟周期要少得多。) 如果您正在使用RDTSC进行微基准测试以进行调优,最好的选择是只使用时钟周期(ticks),而不要试图转换为纳秒。否则,使用高分辨率的库时间函数,如std::chrono或clock_gettime。请参阅faster equivalent of gettimeofday以了解一些关于时间戳函数的讨论和比较,或者从内存中读取共享时间戳以避免完全使用rdtsc,如果您的精度要求足够低,可以使用定时器中断或线程来更新它。
另请参阅Calculate system time using rdtsc以了解如何找到晶体频率和倍频器。

{{link1:在多核多处理器环境中,CPU TSC获取操作特别是在 Nehalem 及更新版本中,TSC在一个封装中的所有核心上都是同步和锁定的(以及不变的=恒定和不停的TSC特性)。有关多套接字同步的一些很好的信息,请参阅@amdn在那里的答案。

(而且显然,只要现代多套接字系统具有该功能,它通常也可靠,参见链接问题上@amdn的答案,以及下面的更多详细信息。)


CPUID与TSC相关的特性

使用Linux /proc/cpuinfo中用于CPU特性的名称,以及您还会找到的同一特性的其他别名。

  • tsc - TSC存在且支持rdtsc。x86-64的基准。
  • rdtscp - 支持rdtscp
  • tsc_deadline_timer CPUID.01H:ECX.TSC_Deadline[bit 24] = 1 - 可以编程本地APIC,在TSC达到您在IA32_TSC_DEADLINE中设置的值时触发中断。启用“无tick”内核,我认为,直到下一个应该发生的事情。
  • constant_tsc:通过检查CPU家族和型号号码来确定是否支持恒定TSC功能。TSC以恒定频率计时,不受核心时钟速度变化的影响。如果没有这个功能,RDTSC会计算核心时钟周期。
  • nonstop_tsc:这个功能在英特尔SDM手册中称为不变TSC,并且在具有CPUID.80000007H:EDX[7]的处理器上受支持。即使在深度睡眠C状态下,TSC仍然在计时。在所有x86处理器上,nonstop_tsc意味着constant_tsc,但constant_tsc不一定意味着nonstop_tsc。没有单独的CPUID特性位;在英特尔和AMD上,相同的不变TSC CPUID位意味着constant_tscnonstop_tsc功能。请参见Linux的x86/kernel/cpu/intel.c检测代码amd.c类似。
一些基于Saltwell/Silvermont/Airmont的处理器(但并非全部)在ACPI S3全系统睡眠中保持TSC的运行:nonstop_tsc_s3。这被称为始终开启的TSC。(尽管似乎基于Airmont的处理器从未发布。)
有关恒定和不变的TSC的更多详细信息,请参阅:Can constant non-invariant tsc change frequency across cpu states?
  • tsc_adjust: CPUID.(EAX=07H, ECX=0H):EBX.TSC_ADJUST (bit 1) 可用的IA32_TSC_ADJUST MSR允许操作系统设置一个偏移量,当rdtscrdtscp读取时,该偏移量将添加到TSC中。这样可以有效地在一些/所有核心上更改TSC,而不会导致逻辑核心之间的不同步(如果软件在每个核心上将TSC设置为新的绝对值,将在每个核心上的同一周期执行相关的WRMSR指令非常困难)。

constant_tscnonstop_tsc一起使得TSC可用作用户空间中的clock_gettime等时间源。 (但是像Linux这样的操作系统只使用RDTSC在较慢的时钟的刻度之间进行插值,通过定时中断更新刻度/偏移因子。请参见在具有constant_tsc和nonstop_tsc的CPU上,为什么我的时间会漂移?)在不支持深度睡眠状态或频率缩放的更旧的CPU上,TSC作为时间源仍然可用

Linux源代码中的注释还指出,constant_tsc / nonstop_tsc功能(在Intel上)意味着“它在核心和插槽之间也是可靠的。(但在机柜之间不可靠 - 我们在这种情况下明确关闭它。)”

"跨套接字"部分不准确。一般来说,不变的TSC只能保证在同一套接字内的核心之间TSC是同步的。在一个英特尔论坛帖子中,马丁·迪克森(Intel)指出,TSC的不变性并不意味着跨套接字的同步。这需要平台供应商将RESET同步分发到所有套接字。显然,根据上述Linux内核注释,平台供应商在实践中确实这样做了。在关于CPU TSC在多核多处理器环境中的获取操作的回答中,也有人同意单个主板上的所有套接字应该起始于同步状态。
在一个多插槽共享内存系统中,没有直接的方法来检查所有核心中的TSC是否同步。Linux内核默认执行引导时和运行时检查,以确保TSC可以用作时钟源。这些检查涉及确定TSC是否同步。命令dmesg | grep 'clocksource'的输出将告诉您内核是否使用TSC作为时钟源,只有在检查通过时才会发生这种情况。但即使如此,这也不能证明TSC在系统的所有插槽上都同步。内核参数tsc=reliable可用于告诉内核可以盲目地使用TSC作为时钟源,而无需进行任何检查。
存在一些情况下,跨插槽的TSC可能不同步:(1)热插拔CPU,(2)插槽分布在由扩展节点控制器连接的不同主板上,(3)在某些处理器中,从C状态唤醒后可能不会重新同步TSC,而在该状态下TSC被关闭,以及(4)不同插槽安装了不同的CPU型号。
一个直接修改TSC而不使用TSC_ADJUST偏移量的操作系统或虚拟机可能会导致它们不同步,因此在用户空间中,不能总是安全地假设CPU迁移不会导致读取不同的时钟。(这就是为什么rdtscp会产生一个额外的核心ID作为输出,这样你就可以检测开始/结束时间是否来自不同的时钟。它可能是在不变TSC特性之前引入的,或者可能只是为了考虑到每一种可能性。)
如果你直接使用rdtsc,你可能希望将你的程序或线程固定到一个核心,例如在Linux上使用taskset -c 0 ./myprogram。无论你是否需要它来获取TSC,CPU迁移通常会导致大量的缓存失效,并且会干扰你的测试,同时还会花费额外的时间。(尽管中断也会导致这种情况发生。)

使用内嵌汇编语言的效率如何?

使用内嵌汇编语言的效率与@Mysticial的GNU C内联汇编语言相当,甚至更好,因为它知道RAX的高位被清零。保留内联汇编语言的主要原因是与老旧编译器的兼容性。

对于x86-64架构,readTSC函数的非内联版本在MSVC上的编译结果如下:

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

对于返回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;
}

所有四个编译器生成的代码非常相似。这是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位存储而不是移位/或/移动指令。如果你对计时中的额外的微操作感到困扰,最好将整个微基准测试编写为纯汇编语言。
然而,我们可以通过修改@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()给出更好的汇编代码,但有时会欺骗编译器使用额外的寄存器来分别保存lo和hi,所以clang可以优化为((end_hi-start_hi)<<32) + (end_lo-start_lo)。希望如果有真正的寄存器压力,编译器会更早地进行合并。(gcc和ICC仍然分别保存lo/hi,但优化效果不如此好。)
但是32位的gcc8会把它搞得一团糟,即使只是编译rdtsc()函数本身,也会用零来进行add/adc,而不是像clang那样只返回edx:eax中的结果。(gcc6及更早版本对|而不是+处理得还可以,但如果你关心gcc生成的32位代码,最好使用__rdtsc()内置函数)。

4
tsc 不一定以 "sticker frequency" 的频率进行滴答,而是以 tsc 的频率进行。在某些机器上,这两者相同,但在许多新机器上(如 Skylake 客户端和派生的微体系结构),它们通常不同。例如,我的 i7-6700HQ 的 sticker frequency 是 2600 MHz,但 tsc 频率为 2592 MHz。如果它们基于的不同时钟不能按整数比例尺度化到完全相同的频率,则它们可能不相同。许多工具没有考虑到这种差异,从而导致小误差。 - BeeOnRope
2
在最近的内核上,您可以执行“dmesg | grep tsc”以查看两个值。我得到了“tsc:检测到2600.000 MHz处理器... tsc:检测到2592.000 MHz TSC”。您还可以使用“turbostat”来显示这个值。 - BeeOnRope
3
仅补充一点,贴纸基础频率、Turbo频率和TSC频率现在已经大相径庭了。i5-1035的TSC频率为1.5 GHz,但其基础频率为1.1 GHz,Turbo频率(并不是真正相关的)则为3.7 GHz。 - BeeOnRope
1
@Arty:你的T4300是一款Penryn CPU,属于第二代Core2处理器,因此如果没有nonstop_tsc,应该具备constant_tsc。我猜测热量调节涉及深度睡眠,暂停时钟以获得某种平均占空比,但低速空闲并不会产生影响。 - Peter Cordes
3
我刚看完这条评论就获得了学士学位。 - jonadv
显示剩余9条评论

10

VC++在使用内嵌汇编时采用完全不同的语法——但仅适用于32位版本。64位编译器根本不支持内嵌汇编。

在这种情况下,这可能是件好事——当涉及到计时代码序列时,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个或更多个时钟周期。显然,在后一种情况下,你只需放弃离群值--它不是来自你的代码。

总之:要成功执行rdtsc指令本身(几乎)是你最不用担心的事情。在从rdtsc中获得真正有意义的结果之前,你需要做很多其他的事情。


我非常确定在研究时,我发现 QueryPerformanceCounter(它是对 rdtsc 的薄纱包装)在多核/多处理器系统上存在您所指出的同样问题。但我认为我也找到了文档,说明这个问题在早期系统上确实存在,因为大多数 BIOS 甚至没有尝试同步不同核心上的计数器,但是大多数较新的 BIOS(也许不包括廉价垃圾机器 BIOS)现在会做出努力,因此它们现在可能只有几个计数的偏差。 - phonetagger
但为了完全避免这种可能性,您可以设置线程的处理器亲和性掩码,使其仅在单个核心上运行,从而完全消除此问题。(我看到您也提到了这一点) - phonetagger
QPC 可以是,但不一定是,对 rdtsc 的轻薄封装。至少在某个时候,单处理器内核使用了 rdtsc,但多处理器内核则使用主板的 1.024 MHz 时钟芯片(正是出于引用原因)。 - Jerry Coffin

6

对于Windows平台,Visual Studio提供了一个方便的“编译器内置”(即一种特殊函数,编译器可以理解)来执行RDTSC指令并返回结果:

unsigned __int64 __rdtsc(void);

6

使用Linux系统调用 perf_event_open 和配置参数为 PERF_COUNT_HW_CPU_CYCLES

这个Linux系统调用看起来是性能事件的跨架构封装。

这个回答类似于快速计算C程序中执行的指令数,但是使用了PERF_COUNT_HW_CPU_CYCLES而不是PERF_COUNT_HW_INSTRUCTIONS。该回答重点介绍了PERF_COUNT_HW_CPU_CYCLES的具体内容,请查看其他回答获取更通用的信息。

这里提供基于man页面末尾提供的示例的一个示例。

perf_event_open.c

#define _GNU_SOURCE
#include <asm/unistd.h>
#include <linux/perf_event.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <unistd.h>

#include <inttypes.h>
#include <sys/types.h>

static long
perf_event_open(struct perf_event_attr *hw_event, pid_t pid,
                int cpu, int group_fd, unsigned long flags)
{
    int ret;

    ret = syscall(__NR_perf_event_open, hw_event, pid, cpu,
                    group_fd, flags);
    return ret;
}

int
main(int argc, char **argv)
{
    struct perf_event_attr pe;
    long long count;
    int fd;

    uint64_t n;
    if (argc > 1) {
        n = strtoll(argv[1], NULL, 0);
    } else {
        n = 10000;
    }

    memset(&pe, 0, sizeof(struct perf_event_attr));
    pe.type = PERF_TYPE_HARDWARE;
    pe.size = sizeof(struct perf_event_attr);
    pe.config = PERF_COUNT_HW_CPU_CYCLES;
    pe.disabled = 1;
    pe.exclude_kernel = 1;
    // Don't count hypervisor events.
    pe.exclude_hv = 1;

    fd = perf_event_open(&pe, 0, -1, -1, 0);
    if (fd == -1) {
        fprintf(stderr, "Error opening leader %llx\n", pe.config);
        exit(EXIT_FAILURE);
    }

    ioctl(fd, PERF_EVENT_IOC_RESET, 0);
    ioctl(fd, PERF_EVENT_IOC_ENABLE, 0);

    /* Loop n times, should be good enough for -O0. */
    __asm__ (
        "1:;\n"
        "sub $1, %[n];\n"
        "jne 1b;\n"
        : [n] "+r" (n)
        :
        :
    );

    ioctl(fd, PERF_EVENT_IOC_DISABLE, 0);
    read(fd, &count, sizeof(long long));

    printf("%lld\n", count);

    close(fd);
}

结果看起来合理,例如如果我打印循环次数,然后重新编译以计算指令数,我们每次迭代大约会得到1个周期(在一个周期内完成2个指令),可能由于超标量执行等影响而产生略微不同的结果。每次运行的结果略有不同,这可能是由于随机内存访问延迟引起的。
您可能还对PERF_COUNT_HW_REF_CPU_CYCLES感兴趣,正如man页记录的那样:
总周期数;不受CPU频率缩放的影响。
如果您启用了频率缩放,则这将给出更接近真实墙时钟时间的结果。在我的快速实验中,它们比PERF_COUNT_HW_INSTRUCTIONS大2/3倍,这可能是因为我的非压力机器现在已被频率缩放。

你应该明确指出核心时钟周期与RDTSC参考周期是不同的。它是实际的CPU周期,而不是某个固定频率的周期,因此在某些情况下更准确地反映了你想要的内容。(但它不会在核心停止运行时打勾,例如在频率转换或睡眠时,因此对于涉及I/O的程序来说,它绝对不是实时测量的标准。) - Peter Cordes
你用这个程序测量到的循环次数比指令多?这可能主要是由于测量开销,因为循环本身应该以1次迭代/周期=2次指令/周期运行。与启用Spectre和Meltdown缓解措施的Linux系统调用开销相比,您默认的“n = 10000”(时钟周期)非常微小。如果您要求perf/PAPI在用户空间中使用“rdpmc”,则可以使用它来进行测量,而不会像“rdtsc”那样产生较少的开销(仍然是CPU周期,而不是参考周期)。 - Peter Cordes
@PeterCordes 感谢您提供的这些指针。也许 PERF_COUNT_HW_REF_CPU_CYCLES 更类似于 RDTSC(“总周期数;不受 CPU 频率缩放影响。”)。请注意,内核空间指令应通过 pe.exclude_kernel = 1; 移除,实验中 10k 已经能够给出代表性结果,并且随着大小呈现出更或多或少线性的变化。我还猜测 RDTSC 和 RDPMC 不区分同时运行的不同进程,尽管它们的开销低于系统调用。 - Ciro Santilli OurBigBook.com
是的,PERF_COUNT_HW_REF_CPU_CYCLES 计数器的频率与 RDTSC 读取的 TSC 相同。(除非在频率转换期间它根本不会计数)。我的观点是,你使用的 PERF_COUNT_HW_CPU_CYCLES 也会在频率转换期间暂停,所以它并不只是以不同单位的挂钟时间。如果在用户空间执行时出现 turbo 频率转换的吞吐量成本将不会被该计数器计算,在这种情况下可能很重要。 - Peter Cordes
1
由于超标量执行 - 技术细节:在英特尔Sandybridge系列CPU上,实际上是由于解码器中的宏融合将sub/jnz转换为单个减法和分支uop。因此,后端每个周期只执行1个uop。这个uop来自uop缓存,所以除了初始解码之外,实际上没有超标量执行:P(除了可能将4个这些uops组成一组发送到后端,然后空闲3个周期)。但如果您有AMD CPU,则只会融合cmp或test,因此这将是真正的超标量执行。 - Peter Cordes
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接