获取CPU周期数?

时间:2012-12-07 23:18:41

标签: c++ c performance x86 rdtsc

我在SO上看到这篇帖子,其中包含用于获取最新CPU周期数的C代码:

CPU Cycle count based profiling in C/C++ Linux x86_64

有没有办法在C ++中使用这段代码(欢迎使用windows和linux解决方案)?虽然用C语言编写(而C是C ++的一个子集)但我不太确定这段代码是否适用于C ++项目,如果没有,如何翻译呢?

我正在使用x86-64

EDIT2:

找到此功能但无法让VS2010识别汇编程序。我需要包含任何内容吗? (我相信我必须将uint64_t换成long long以获取Windows ....?)

static inline uint64_t get_cycles()
{
  uint64_t t;
  __asm volatile ("rdtsc" : "=A"(t));
  return t;
}

EDIT3:

从上面的代码我得到错误:

  

“错误C2400:'操作码'中的内联汇编语法错误;找到'数据   类型'“

有人可以帮忙吗?

5 个答案:

答案 0 :(得分:50)

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

但是需要的包含是不同的:

#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

答案 1 :(得分:15)

您不需要此内联汇编。。没有好处。编译器具有rdtscrdtscp的内置程序,并且(至少最近这些天)都定义了__rdtsc内在函数(如果您包括正确的头文件)。但是与几乎所有其他情况(https://gcc.gnu.org/wiki/DontUseInlineAsm)不同的是,只要您使用的是@ Mysticial's 之类的良好且安全的实现,而{{3 }}。

不幸的是,MSVC与其他所有人不同意在非SIMD内部函数使用哪个标头。

a broken "=A" constraint_rdtsc(带有一个下划线)在<immintrin.h>中,但是在gcc和clang上不起作用。它们仅在<immintrin.h>中定义SIMD内在函数,因此我们陷入<intrin.h>(MSVC)与<x86intrin.h>(其他所有因素,包括最近的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;
    return __rdtscp(&dummy);  // waits for earlier insns to retire, but allows later to start
}

可使用所有4种主要编译器进行编译:gcc / clang / ICC / MSVC,适用于32位或64位。请参见Intel's intriniscs guide,包括几个测试调用程序。

这些内在函数是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支持内联asm。 ICC13在__rdtsc中有immintrin.h,但根本没有x86intrin.h。最近的ICC拥有x86intrin.h,至少是Godbolt为其在Linux上安装它们的方式。

您可能希望将它们定义为带符号的long long ,尤其是要减去它们并将其转换为float时。在没有AVX512的x86上,int64_t-> float / double比uint64_t更有效。另外,如果TSC不能完全同步,则由于CPU迁移,可能会产生小的负面结果,这可能比无符号大数字更有意义。


顺便说一句,clang还具有可移植的__builtin_readcyclecounter(),可在任何体系结构上使用。 (在没有循环计数器的体系结构上,始终返回零。)请参见the results on the Godbolt compiler explorer


有关使用lfence(或cpuid)来提高rdtsc的可重复性并通过阻止超时来精确控制哪些指令不在时间间隔内的详细信息, ,请参见@HadiBrais在the clang/LLVM language-extension docs上的回答以及有关其区别的示例。

另请参见clflush to invalidate cache line via C function(启用Spectre缓解功能的TL:DR是,否则内核将未设置相关的MSR,因此您应使用cpuid进行序列化。)在Intel上,它始终被定义为部分序列化

Is LFENCE serializing on AMD processors? ,这是2010年发布的英特尔白皮书。


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

无论turbo /节能模式如何,它都以固定的频率进行计数,因此,如果要进行每时钟一次的分析,请使用性能计数器。 rdtsc与挂钟时间完全相关(系统时钟调整除外,因此它是steady_clock的理想时间源)。它在CPU的额定频率(即广告标贴频率)上滴答作响。 (或者接近,例如在i7-6700HQ 2.6 GHz Skylake上为2592 MHz。)

如果将其用于微基准测试,请在开始计时之前先包括一个预热时间,以确保您的CPU已经达到最大时钟速度。 (并有选择地禁用turbo,并告诉您的操作系统首选最大时钟速度,以避免在微基准测试期间CPU频率偏移)。或更妙的是,使用可以访问硬件性能计数器的库,或者使用How to Benchmark Code Execution Times on Intel® IA-32 and IA-64 Instruction Set Architectures之类的技巧,如果您的定时区域足够长,可以附加一个perf stat -p PID

但是,除非您想了解在内存受限或其他情况下如何使Skylake降低时钟频率,否则通常仍然需要为微基准保持CPU时钟固定。 (请注意,内存带宽/等待时间大部分是固定的,使用与内核不同的时钟。在空闲时钟速度下,L2或L3高速缓存未命中占用的内核时钟周期要少得多。)

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

Calculate system time using rdtsc说, Nehalem和更新的版本将TSC同步并锁定到包中的所有内核(即不变的TSC)。但是多路系统仍然是一个问题。甚至更老的系统(如2007年的Core2之前的系统)也可能具有TSC,该TSC在核心时钟停止时停止,或者与实际核心时钟频率(而不是参考周期)相关联。 (较新的CPU始终具有恒定TSC和不停止TSC。)有关更多详细信息,请参见@amdn对该问题的回答。


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

它与您从@Mysticial的GNU C内联汇编中获得的效果差不多,或者更好,因为它知道RAX的高位为零。您想要保持内联汇编的主要原因是与硬性旧编译器兼容。

taskset -c 0 ./myprogram函数的非内联版本本身使用x86-64的MSVC进行编译,如下所示:

readTSC

对于在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

所有4个编译器都编写非常相似的代码。这是GCC的32位输出:

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

这是MSVC的x86-64输出(已应用名称分解)。 gcc / clang / ICC都发出相同的代码。

# 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

所有4个编译器都使用# 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 + or而不是mov将上下半部分组合到一个不同的寄存器中。我想这是他们无法优化的固定顺序。

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

但是,通过修改@Mysticial的代码,我们也许可以两全其美:

lea

CPU TSC fetch operation especially in multicore-multi-processor environment,对于gcc / clang / ICC,这有时会提供比// 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 } 更好的asm,但有时它会欺骗编译器使用额外的寄存器分别保存lo和hi,因此clang可以优化进入__rdtsc()。希望如果存在真正的寄存器压力,编译器将更早合并。 (gcc和ICC仍然分别保存lo / hi,但也没有进行优化。)

但是32位gcc8搞砸了它,甚至只将((end_hi-start_hi)<<32) + (end_lo-start_lo)函数本身与实际的rdtsc()一起编译为零,而不是像clang一样仅在edx:eax中返回结果。 (gcc6和更早版本可以使用add/adc而不是|,但如果您关心gcc的32位代码生成,则肯定更喜欢+内在函数。)

答案 2 :(得分:6)

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在代码完成后得到时间。

除此之外,您还希望使用操作系统提供的任何方式强制所有操作在一个进程/核心上运行。在大多数情况下,您还希望强制代码对齐 - 对齐方式的更改可能会导致执行语言中存在相当大的差异。

最后你想多次执行它 - 并且它总是可能在事物中间被中断(例如,任务切换),所以你需要为执行的可能性做好准备比其余部分长得多 - 例如,每次运行需要约40-43个时钟周期的5次运行,以及需要超过10000个时钟周期的第6次运行。显然,在后一种情况下,你只是抛弃异常值 - 它不是来自你的代码。

总结:管理执行rdtsc指令本身(几乎)是你最不担心的事情。在你从rdtsc获得实际上意味着什么的结果之前,你需要做更多的事情。

答案 3 :(得分:5)

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

unsigned __int64 __rdtsc(void);

答案 4 :(得分:1)

使用perf_event_open的Linux config = PERF_COUNT_HW_CPU_CYCLES系统调用

此Linux系统调用似乎是性能事件的跨体系结构包装。

答案类似:Quick way to count number of instructions executed in a C program,但用PERF_COUNT_HW_CPU_CYCLES代替了PERF_COUNT_HW_INSTRUCTIONS。该答案将重点放在PERF_COUNT_HW_CPU_CYCLES的细节上,请参见其他详细信息。

perf_event_open.c

#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>

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(作为联机帮助文档)感兴趣:

总周期;不受CPU频率缩放的影响。

因此,如果您启用频率缩放功能,这将使实际壁挂时间更接近实际时间。在我的快速实验中,它们比PERF_COUNT_HW_INSTRUCTIONS大2/3倍,大概是因为我的非受力机器现在已按频率缩放。