对于 x86-64,您的内联汇编已损坏。 "=A"
在 64 位模式下,编译器可以选择RAX或 RDX,而不是 EDX:EAX。有关更多信息,请参阅此问答
你不需要内联汇编。没有任何好处;编译器内置了rdtsc
and rdtscp
,并且(至少现在)__rdtsc
如果您包含正确的标头,则它们都定义了一个内在函数。但与几乎所有其他情况(https://gcc.gnu.org/wiki/DontUseInlineAsm)不同,asm 没有严重的缺点,只要您使用像 @Mysticial's 这样的良好且安全的实现。
(asm 的一个小优势是,如果您想计时一个肯定会小于 2^32 计数的小间隔,您可以忽略结果的高半部分。编译器可以uint32_t time_low = __rdtsc()
使用内在函数为您进行优化,但在练习他们有时仍然会浪费指令做换档/或。)
不幸的是,MSVC 对于非 SIMD 内部函数使用哪个标头不同意其他所有人。
英特尔的 intriniscs 指南说_rdtsc
(带有一个下划线) in <immintrin.h>
,但这不适用于 gcc 和 clang。他们只在 中定义 SIMD 内在函数<immintrin.h>
,所以我们坚持使用<intrin.h>
(MSVC) 与<x86intrin.h>
(其他一切,包括最近的 ICC)。为了与 MSVC 和 Intel 的文档兼容,gcc 和 clang 定义了函数的单下划线和双下划线版本。
有趣的事实:双下划线版本返回一个无符号的 64 位整数,而 Intel 文档_rdtsc()
返回 (signed) __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 位。在 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
in immintrin.h
,但根本没有 a x86intrin.h
。最近的 ICC 有x86intrin.h
,至少是 Godbolt 为 Linux 安装它们的方式。
您可能希望将它们定义为有符号long long
,特别是如果您想减去它们并转换为浮点数。 int64_t
-> float/double 比uint64_t
没有 AVX512 的 x86 更有效。此外,如果 TSC 没有完全同步,由于 CPU 迁移,可能会出现小的负面结果,这可能比巨大的无符号数字更有意义。
顺便说一句,clang 还有一个__builtin_readcyclecounter()
适用于任何架构的便携式设备。(在没有循环计数器的架构上总是返回零。)请参阅clang/LLVM 语言扩展文档
有关使用lfence
(or cpuid
) 通过阻止乱序执行来提高可重复性rdtsc
并准确控制哪些指令在定时间隔内/不在定时间隔内的更多信息,请参阅@HadiBrais 对 clflush 的回答,以通过 C 函数和评论它所产生的差异的一个例子。
另请参阅LFENCE 是否在 AMD 处理器上进行序列化?(TL:DR 是的,启用 Spectre 缓解,否则内核会保留相关的 MSR 未设置,因此您应该使用cpuid
它来进行序列化。)它一直被定义为 Intel 上的部分序列化。
如何在英特尔® IA-32 和 IA-64 指令集架构上对代码执行时间进行基准测试,这是 2010 年的英特尔白皮书。
rdtsc
计算参考周期,而不是 CPU 核心时钟周期
无论涡轮/省电如何,它都以固定频率计数,因此如果您想要按时钟进行 uops-per-clock 分析,请使用性能计数器。 rdtsc
与挂钟时间完全相关(不计算系统时钟调整,因此它是 的完美时间源steady_clock
)。
TSC 频率过去总是等于 CPU 的额定频率,即标榜的标签频率。在某些 CPU 中,它只是接近,例如 i7-6700HQ 2.6 GHz Skylake 上的 2592 MHz,或 4000 MHz i7-6700k 上的 4008 MHz。在 i5-1035 Ice Lake 等更新的 CPU 上,TSC = 1.5 GHz,base = 1.1 GHz,因此禁用 turbo 甚至对于这些 CPU 上的 TSC = 核心周期几乎都不起作用。
如果您将其用于微基准测试,请先包含一个预热期,以确保您的 CPU 在开始计时之前已经处于最大时钟速度。(并且可以选择禁用 turbo 并告诉您的操作系统更喜欢最大时钟速度,以避免在您的微基准测试期间 CPU 频率偏移)。
微基准测试很难:见惯用的性能评估方式?对于其他陷阱。
除了 TSC 之外,您还可以使用一个库来访问硬件性能计数器。复杂但开销低的方法是编写 perf 计数器并rdmsr
在用户空间中使用,或者更简单的方法包括perf stat 之类的技巧,如果您的定时区域足够长,您可以附加一个perf stat -p PID
.
不过,您通常仍希望为微基准测试保持 CPU 时钟固定,除非您想了解不同的负载如何让 Skylake 在内存受限或其他情况下时钟下降。(请注意,内存带宽/延迟大部分是固定的,使用与内核不同的时钟。在空闲时钟速度下,L2 或 L3 缓存未命中需要的内核时钟周期要少得多。)
如果您出于调整目的使用 RDTSC 进行微基准测试,那么最好的选择是只使用滴答声并跳过甚至尝试转换为纳秒。 否则,请使用高分辨率库时间函数,如std::chrono
or clock_gettime
。有关时间戳函数的一些讨论/比较,请参阅gettimeofday 的更快等效项rdtsc
,或者如果您的精度要求足够低以使计时器中断或线程更新它,则从内存中读取共享时间戳以完全避免。
另请参阅使用 rdtsc 计算系统时间,了解查找晶体频率和乘数。
CPU TSC 获取操作,尤其是在多核-多处理器环境中表示Nehalem 和更新的 TSC 为一个包中的所有内核同步并锁定在一起(以及不变 = 恒定和不间断的 TSC 功能)。有关多套接字同步的一些有用信息,请参阅@amdn 的答案。
(显然,即使对于现代多插槽系统,只要它们具有该功能,它们通常也是可靠的,请参阅@amdn 对链接问题的回答,以及下面的更多详细信息。)
与 TSC 相关的 CPUID 功能
使用Linux/proc/cpuinfo
用于 CPU features的名称,以及您还将找到的相同功能的其他别名。
tsc
- TSC 存在并rdtsc
受支持。x86-64 的基线。
rdtscp
-rdtscp
支持。
tsc_deadline_timer
CPUID.01H:ECX.TSC_Deadline[bit 24] = 1
- 本地 APIC 可以编程为在 TSC 达到您输入的值时触发中断IA32_TSC_DEADLINE
。我认为,启用“tickless”内核,直到下一件应该发生的事情发生。
constant_tsc
:对恒定 TSC 功能的支持是通过检查 CPU 系列和型号来确定的。无论核心时钟速度如何变化,TSC 都以恒定频率滴答作响。没有这个,RDTSC会计算核心时钟周期。
nonstop_tsc
:此功能在英特尔 SDM 手册中称为不变 TSC,并且在带有CPUID.80000007H:EDX[8]
. 即使在深度睡眠 C 状态下,TSC 也会保持滴答作响。在所有 x86 处理器上,nonstop_tsc
暗示constant_tsc
但constant_tsc
不一定暗示nonstop_tsc
. 没有单独的 CPUID 功能位;在 Intel 和 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 的更多详细信息,请参阅:常数非不变 tsc 可以在 cpu 状态之间改变频率吗?.
tsc_adjust
:MSR 可用,允许操作系统设置偏移量,在读取或读取它时添加CPUID.(EAX=07H, ECX=0H):EBX.TSC_ADJUST (bit 1)
到TSC 。这允许有效地更改某些/所有内核上的 TSC,而无需在逻辑内核之间取消同步。(如果软件在每个内核上将 TSC 设置为新的绝对值,就会发生这种情况;很难在每个内核上以相同的周期执行相关的 WRMSR 指令。)IA32_TSC_ADJUST
rdtsc
rdtscp
constant_tsc
并nonstop_tsc
共同使 TSC 可用作clock_gettime
用户空间等事物的时间源。(但像 Linux 这样的操作系统只使用 RDTSC 在由 NTP 维护的较慢时钟的滴答之间进行插值,更新定时器中断中的比例/偏移因子。请参阅On a cpu with constant_tsc 和 nonstop_tsc,为什么我的时间会漂移?)在更旧的 CPU 上不支持深度睡眠状态或频率缩放,TSC 作为时间源可能仍然可用
Linux 源代码中的注释还表明constant_tsc
/ nonstop_tsc
features(在 Intel 上)意味着“它在内核和套接字之间也是可靠的。(但不是跨机柜 - 在这种情况下我们明确将其关闭。) ”
“跨套接字”部分不准确。通常,不变的 TSC 仅保证 TSC 在同一插槽内的内核之间同步。在英特尔论坛主题中,Martin Dixon(英特尔)指出TSC 不变性并不意味着跨插槽同步。这需要平台供应商将 RESET 同步分发到所有套接字。鉴于上述 Linux 内核评论, 显然平台供应商在实践中会这样做。关于 CPU TSC 获取操作的答案,特别是在多核多处理器环境中,也同意单个主板上的所有插槽应该同步启动。
在多插槽共享内存系统上,没有直接的方法可以检查所有内核中的 TSC 是否同步。Linux 内核默认执行启动时和运行时检查以确保 TSC 可以用作时钟源。这些检查涉及确定 TSC 是否已同步。该命令的输出dmesg | grep 'clocksource'
将告诉您内核是否使用 TSC 作为时钟源,只有在检查通过时才会发生这种情况。但即便如此,这也不能明确证明 TSC 在系统的所有套接字之间是同步的。内核参数tsc=reliable
可以用来告诉内核它可以盲目地使用TSC作为时钟源而不做任何检查。
在某些情况下,跨插槽 TSC 可能不同步:(1) 热插拔 CPU,(2) 当插槽分布在由扩展节点控制器连接的不同板上时,(3) TSC 在唤醒后可能不会重新同步从某些处理器中 TSC 断电的 C 状态开始,并且 (4) 不同的插槽安装了不同的 CPU 型号。
直接更改 TSC 而不是使用 TSC_ADJUST 偏移量的操作系统或管理程序可以取消同步它们,因此在用户空间中,假设 CPU 迁移不会让您读取不同的时钟可能并不总是安全的。(这就是为什么rdtscp
生成一个核心 ID 作为额外输出的原因,因此您可以检测开始/结束时间何时来自不同的时钟。它可能是在不变 TSC 功能之前引入的,或者他们只是想考虑所有可能性。 )
如果您rdtsc
直接使用,您可能希望将您的程序或线程固定到核心,例如taskset -c 0 ./myprogram
在 Linux 上。无论您是否需要 TSC,CPU 迁移通常会导致大量缓存未命中,并且无论如何都会弄乱您的测试,并且需要额外的时间。(尽管中断也会如此)。
使用内在函数的 asm 效率如何?
它与您从@Mysticial 的 GNU C 内联汇编中获得的一样好,或者更好,因为它知道 RAX 的高位被归零。您想要保留内联 asm 的主要原因是为了与顽固的旧编译器兼容。
函数本身的非内联版本readTSC
使用 MSVC for x86-64 编译,如下所示:
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;
}
所有 4 个编译器都编写了非常相似的代码。这是 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
将低半部分和高半部分组合到不同的寄存器中。我猜这是他们未能优化的固定序列。
但是自己在 inline asm 中编写 shift/lea 也好不到哪里去。如果您的时间间隔如此之短以至于您只保留 32 位结果,那么您将剥夺编译器忽略 EDX 中结果的高 32 位的机会。或者如果编译器决定将开始时间存储到内存中,它可以只使用两个 32 位存储而不是 shift/或/mov。如果 1 个额外的 uop 作为计时的一部分让您感到困扰,您最好用纯 asm 编写整个微基准测试。
然而,我们也许可以通过 @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 上,这有时会提供比__rdtsc()
gcc/clang/ICC 更好的 asm,但有时它会欺骗编译器使用额外的寄存器来分别保存 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()