use*_*112 26 c c++ performance x86 rdtsc
我在SO上看到这篇文章,其中包含C代码以获取最新的CPU周期数:
基于CPU周期计算的C/C++ Linux x86_64中的分析
有没有办法在C++中使用这个代码(欢迎使用windows和linux解决方案)?虽然用C语言编写(而C是C++的一个子集)但我不太确定这段代码是否适用于C++项目,如果没有,如何翻译呢?
我使用的是x86-64
EDIT2:
找到此功能但无法让VS2010识别汇编程序.我需要包含任何内容吗?(我相信我必须换uint64_t到long long窗户......?)
static inline uint64_t get_cycles()
{
uint64_t t;
__asm volatile ("rdtsc" : "=A"(t));
return t;
}
Run Code Online (Sandbox Code Playgroud)
EDIT3:
从上面的代码我得到错误:
"错误C2400:'操作码'中的内联汇编语法错误;找到'数据类型'"
有人可以帮忙吗?
Mys*_*ial 51
从GCC 4.5及更高版本开始,__rdtsc()现在MSVC和GCC都支持内在函数.
但所需的包含是不同的:
#ifdef _WIN32
#include <intrin.h>
#else
#include <x86intrin.h>
#endif
Run Code Online (Sandbox Code Playgroud)
这是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
Run Code Online (Sandbox Code Playgroud)
Pet*_*des 21
你不需要内联asm.没有好处; 编译器已经内置插件的"=A"和rdtsc,和(至少这些天)都定义了一个rdtscp内在的,如果你有正确的头.但与几乎所有其他情况(https://gcc.gnu.org/wiki/DontUseInlineAsm)不同,asm并没有严重的缺点,只要你使用像@ Mysticial这样的好的和安全的实现,而不是一个破坏的__rdtsc约束.
不幸的是,MSVC不同意其他人关于哪些标头用于非SIMD内在函数.
英特尔的intriniscs指南说_rdtsc(有一个下划线)<immintrin.h>,但这对gcc和clang不起作用.他们只定义了SIMD内在函数<immintrin.h>,因此我们坚持使用<intrin.h>(MSVC)与<x86intrin.h>(其他所有内容,包括最近的ICC).为了兼容MSVC和英特尔的文档,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
}
Run Code Online (Sandbox Code Playgroud)
编译所有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安装它们的方式.
您可能希望将它们定义为signedlong long,特别是如果要将它们减去并转换为float. int64_t- > float/double比uint64_t没有AVX512的x86 更有效.此外,如果TSC未完全同步,那么由于CPU迁移可能会产生小的负面结果,这可能比大的无符号数更有意义.
BTW,clang还有一个便携式__builtin_readcyclecounter()适用于任何架构.(对于没有循环计数器的体系结构,始终返回零.)请参阅clang/LLVM语言扩展文档
有关使用lfence(或cpuid)提高可重复性rdtsc并通过阻止无序执行来精确控制哪些指令在定时间隔内的控制的更多信息,请参阅@HadiBrais对clflush的回答以通过C函数使高速缓存行无效并且评论它所带来的差异的一个例子.
另请参阅AMD处理器上的LFENCE序列化吗?(TL:DR是启用了Spectre缓解,否则内核将保留相关的MSR,因此您应该使用cpuid序列化.)它始终被定义为在Intel上进行部分序列化.
如何在英特尔®IA-32和IA-64指令集架构上对代码执行时间进行基准测试,这是2010年的英特尔白皮书.
rdtsc计算参考周期,而不是CPU核心时钟周期无论涡轮/省电,它都以固定频率计数,因此如果您需要每时钟uops分析,请使用性能计数器. rdtsc与挂钟时间完全相关(系统时钟调整除外,因此它是完美的时间源steady_clock).它以CPU的额定频率(即广告贴纸频率)为准.(或者几乎就是这样.例如i7-6700HQ 2.6 GHz Skylake上的2592 MHz.)
如果将其用于微基准测试,请首先包括预热时间段,以确保在开始计时之前CPU已处于最大时钟速度.(并且可选择禁用turbo并告诉操作系统更喜欢最大时钟速度,以避免在微基准测试期间CPU频率偏移).或者更好的是,使用一个可以访问硬件性能计数器的库,或者如果你的定时区域足够长,你可以附加一个程序,就可以使用像程序部分的perf stat这样的技巧perf stat -p PID.
但是,你通常仍然希望为微基准测试保持固定的CPU时钟,除非你想看看不同的负载如何让Skylake在内存受限或其他情况下降低时钟.(请注意,内存带宽/延迟大多是固定的,使用与内核不同的时钟.在空闲时钟速度下,L2或L3缓存未命中会占用更少的内核时钟周期.)
constant_tsc)分离,当时钟停止(nonstop_tsc)时不会停止.还有一些提示,例如,不要采取平均时间,取中位数(将有非常高的异常值).cli),以及rdtscVM下的虚拟化.当然,基本的东西,如常规中断是可能的,所以重复你的时间很多次,扔掉异常值.确定Linux上的TSC频率. 以编程方式查询TSC频率很难并且可能不可能,尤其是在用户空间中,或者可能比校准它更糟糕.使用另一个已知的时间源来校准它需要时间.有关将TSC转换为纳秒的难易程度,请参阅该问题(如果您可以询问操作系统的转换率是多少,那将会很好,因为操作系统已经在启动时执行了此操作).
如果您使用RDTSC进行微基准测试以进行调整,那么您最好的选择是使用刻度线并跳过甚至尝试转换为纳秒. 否则,使用像std::chrono或的高分辨率库时间函数clock_gettime.对于时间戳函数的某些讨论/比较,或者从内存中读取共享时间戳,请查看更快的等效gettimeofday,以便rdtsc在您的精度要求足够低以使定时器中断或线程更新时完全避免.
另请参阅使用rdtsc计算系统时间,了解有关查找晶体频率和乘数的信息.
它也不能保证所有内核的TSC同步.因此,如果您的线程迁移到另一个CPU核心之间__rdtsc(),则可能存在额外的偏差.(但是,大多数操作系统都会尝试同步所有内核的TSC,所以通常它们会非常接近.)如果您rdtsc直接使用,可能需要将程序或线程固定到核心,例如taskset -c 0 ./myprogram在Linux上.
CPU TSC fetch operation especially in multicore-multi-processor environment says that Nehalem and newer have the TSC synced and locked together for all cores in a package (i.e. invariant TSC). But multi-socket systems can still be a problem. Even older systems (like before Core2 in 2007) might have a TSC that stops when the core clock stops, or that's tied to the actual core clock frequency instead of reference cycles. (Newer CPUs always have constant-TSC and non-stop-TSC.) See @amdn's answer on that question for more details.
It's about as good as you'd get from @Mysticial's GNU C inline asm, or better because it knows the upper bits of RAX are zeroed. The main reason you'd want to keep inline asm is for compat with crusty old compilers.
A non-inline version of the readTSC function itself compiles with MSVC for x86-64 like this:
unsigned __int64 readTSC(void) PROC ; readTSC
rdtsc
shl rdx, 32 ; 00000020H
or rax, rdx
ret 0
; return in RAX
Run Code Online (Sandbox Code Playgroud)
For 32-bit calling conventions that return 64-bit integers in edx:eax, it's just rdtsc/ret. Not that it matters, you always want this to inline.
In a test caller that uses it twice and subtracts to time an interval:
uint64_t time_something() {
uint64_t start = readTSC();
// even when empty, back-to-back __rdtsc() don't optimize away
return readTSC() - start;
}
Run Code Online (Sandbox Code Playgroud)
All 4 compilers make pretty similar code. This is GCC's 32-bit output:
# 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
Run Code Online (Sandbox Code Playgroud)
This is MSVC's x86-64 output (with name-demangling applied). gcc/clang/ICC all emit identical code.
# 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
Run Code Online (Sandbox Code Playgroud)
All 4 compilers use or+mov instead of lea to combine the low and high halves into a different register. I guess it's kind of a canned sequence that they fail to optimize.
But writing a shift/lea in inline asm yourself is hardly better. You'd deprive the compiler of the opportunity to ignore the high 32 bits of the result in EDX, if you're timing such a short interval that you only keep a 32-bit result. Or if the compiler decides to store the start time to memory, it could just use two 32-bit stores instead of shift/or/mov. If 1 extra uop as part of your timing bothers you, you'd better write your whole microbenchmark in pure asm.
However, we can maybe get the best of both worlds with a modified version of @Mysticial's code:
// 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
}
Run Code Online (Sandbox Code Playgroud)
On Godbolt, this does sometimes give better asm than __rdtsc() for gcc/clang/ICC, but other times it tricks compilers into using an extra register to save lo and hi separately, so clang can optimize into ((end_hi-start_hi)<<32) + (end_lo-start_lo). Hopefully if there's real register pressure, compilers will combine earlier. (gcc and ICC still save lo/hi separately, but don't optimize as well.)
But 32-bit gcc8 makes a mess of it, compiling even just the rdtsc() function itself with an actual add/adc with zeros instead of just returning the result in edx:eax like clang does. (gcc6 and earlier do ok with | instead of +, but definitely prefer the __rdtsc() intrinsic if you care about 32-bit code-gen from gcc).
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
Run Code Online (Sandbox Code Playgroud)
我知道这看起来很奇怪,但它确实是对的.您执行CPUID是因为它是一个序列化指令(不能无序执行),并且在用户模式下可用.你在开始计时之前执行了三次,因为英特尔记录了第一次执行可以/将以不同于第二次执行的速度执行的事实(他们推荐的是三次,所以三次执行).
然后你执行你的代码测试,另一个cpuid强制序列化,最后的rdtsc在代码完成后得到时间.
除此之外,您还希望使用操作系统提供的任何方法来强制所有操作在一个进程/核心上运行.在大多数情况下,您还希望强制执行代码对齐 - 对齐方式的更改可能会导致执行语言中存在相当大的差异.
最后你想多次执行它 - 它总是有可能在事物中间被中断(例如,一个任务切换),所以你需要为执行的可能性做好准备,需要花费很多时间比其余的更长 - 例如,每次需要大约40-43个时钟周期的5次运行,以及需要超过10000个时钟周期的第6次运行.显然,在后一种情况下,你只是抛弃异常值 - 它不是来自你的代码.
总结:管理执行rdtsc指令本身(几乎)是您最不担心的问题.你需要做的还有更多,才能从中得到结果,rdtsc这实际上意味着什么.
Linuxperf_event_open系统调用config = PERF_COUNT_HW_CPU_CYCLES
这个 Linux 系统调用似乎是性能事件的跨架构包装器。
这个答案类似:Quick way to count number of instructionsexecute in a C program但用PERF_COUNT_HW_CPU_CYCLES代替PERF_COUNT_HW_INSTRUCTIONS。这个答案将重点关注PERF_COUNT_HW_CPU_CYCLES细节,请参阅其他答案以获取更多通用信息。
下面是一个基于手册页末尾提供的示例。
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);
}
Run Code Online (Sandbox Code Playgroud)
结果似乎是合理的,例如,如果我打印周期然后重新编译指令计数,则每次迭代我们得到大约 1 个周期(单个周期内完成 2 条指令),可能是由于超标量执行等影响,每次运行的结果可能略有不同随机存储器访问延迟。
PERF_COUNT_HW_REF_CPU_CYCLES您可能还对,其联机帮助页文档感兴趣:
总周期;不受CPU频率调整的影响。
因此,如果您的频率缩放处于打开状态,这将提供更接近真实壁时间的时间。这些比我的快速实验大 2/3 倍PERF_COUNT_HW_INSTRUCTIONS,大概是因为我的无压力机器现在已进行频率缩放。
对于Windows,Visual Studio提供了一个方便的"编译器内在"(即编译器能够理解的特殊函数),它为您执行RDTSC指令并返回结果:
unsigned __int64 __rdtsc(void);
Run Code Online (Sandbox Code Playgroud)
| 归档时间: |
|
| 查看次数: |
40622 次 |
| 最近记录: |