你的x86-64的内联汇编有问题。在64位模式下,
"=A"
让编译器选择RAX或RDX中的一个,而不是EDX:EAX。请参考
这个问答了解更多信息。
你不需要使用内联汇编。这没有任何好处;编译器已经内置了
rdtsc
和
rdtscp
的功能,并且(至少现在)都定义了
__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
。
#include <stdint.h>
#ifdef _MSC_VER
# include <intrin.h>
#else
# include <x86intrin.h>
#endif
inline
uint64_t readTSC() {
uint64_t tsc = __rdtsc();
return tsc;
}
inline
uint64_t readTSCp() {
unsigned dummy;
uint64_t tsc = __rdtscp(&dummy);
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_tsc
和nonstop_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允许操作系统设置一个偏移量,当rdtsc
或rdtscp
读取时,该偏移量将添加到TSC中。这样可以有效地在一些/所有核心上更改TSC,而不会导致逻辑核心之间的不同步(如果软件在每个核心上将TSC设置为新的绝对值,将在每个核心上的同一周期执行相关的WRMSR指令非常困难)。
constant_tsc
和nonstop_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();
return readTSC() - start;
}
所有四个编译器生成的代码非常相似。这是GCC的32位输出:
time_something():
push ebx
rdtsc
mov ecx, eax
mov ebx, edx
rdtsc
sub eax, ecx
sbb edx, ebx
pop ebx
ret
这是MSVC的x86-64输出(应用了名称解缠)。gcc/clang/ICC都会生成相同的代码。
# MSVC 19 2017 -Ox
unsigned __int64 time_something(void) PROC
rdtsc
shl rdx, 32
or rax, rdx
mov rcx, rax
rdtsc
shl rdx, 32
or rax, rdx
sub rax, rcx
ret 0
unsigned __int64 time_something(void) ENDP
所有4个编译器都使用
or
+
mov
而不是
lea
将低半部分和高半部分合并到不同的寄存器中。我猜这是一种固定的序列,它们无法进行优化。
但是自己在内联汇编中编写移位/加载指令几乎没有更好的效果。如果你的计时间隔很短,只保留32位结果,你会剥夺编译器忽略EDX中高32位结果的机会。或者如果编译器决定将起始时间存储到内存中,它可以只使用两个32位存储而不是移位/或/移动指令。如果你对计时中的额外的微操作感到困扰,最好将整个微基准测试编写为纯汇编语言。
然而,我们可以通过修改@Mysticial的代码来兼顾两者的优点。
uint64_t rdtsc(){
unsigned long lo,hi;
__asm__ __volatile__ ("rdtsc" : "=a" (lo), "=d" (hi));
return ((uint64_t)hi << 32) + lo;
}
在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()
内置函数)。
uint64_t
,您应该#include <stdint.h>
(实际上是<cstdint>
,但您的编译器可能太旧无法支持它)。 - Nikos C.