我在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:'操作码'中的内联汇编语法错误;找到'数据 类型'“
有人可以帮忙吗?
答案 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)
您不需要此内联汇编。。没有好处。编译器具有rdtsc
和rdtscp
的内置程序,并且(至少最近这些天)都定义了__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高速缓存未命中占用的内核时钟周期要少得多。)
constant_tsc
)分离,而在时钟停止(nonstop_tsc
)时并没有停止。还有一些技巧,例如不要花费平均时间,而取中位数(会有非常高的离群值)。cli
的内核模式下也无法避免),以及rdtsc
在虚拟机下的虚拟化。当然,还可以进行诸如常规中断之类的基本操作,因此请重复多次计时并丢弃异常值。measuring code execution times in C using RDTSC instruction。 以编程方式查询TSC频率非常困难,而且可能无法实现,尤其是在用户空间中,或者比校准它可能会产生更糟糕的结果。使用另一个已知的时间源进行校准需要花费时间。有关将TSC转换为纳秒的难度的更多信息,请参见该问题(如果您问操作系统,转换率是多少,这很好,因为操作系统已在启动时进行了转换。)
如果您出于调整目的而使用RDTSC进行微基准测试,则最好的选择是仅使用刻度,甚至跳过尝试转换为纳秒的时间。否则,请使用诸如{ {1}}或std::chrono
。有关时间戳功能的一些讨论/比较,请参见Determine TSC frequency on Linux,或者如果您的精度要求低到足以使计时器中断或线程更新精度的程度,请从存储器中读取共享时间戳,以完全避免clock_gettime
。
另请参阅faster equivalent of gettimeofday,以了解晶振频率和倍频。
也不能保证所有内核的TSC都是同步的。因此,如果您的线程在rdtsc
之间迁移到另一个CPU内核,则可能会有额外的偏差。 (不过,大多数操作系统都尝试同步所有内核的TSC,因此通常它们会非常接近。)如果直接使用__rdtsc()
,则可能要将程序或线程固定到某个内核,例如在Linux上使用rdtsc
。
Calculate system time using rdtsc说, Nehalem和更新的版本将TSC同步并锁定到包中的所有内核(即不变的TSC)。但是多路系统仍然是一个问题。甚至更老的系统(如2007年的Core2之前的系统)也可能具有TSC,该TSC在核心时钟停止时停止,或者与实际核心时钟频率(而不是参考周期)相关联。 (较新的CPU始终具有恒定TSC和不停止TSC。)有关更多详细信息,请参见@amdn对该问题的回答。
它与您从@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倍,大概是因为我的非受力机器现在已按频率缩放。