您的第一个代码(导致标题问题)有问题,因为它用EAX、EBX、ECX 和 EDX 中的结果覆盖了rdtsc
和rdtscp
结果。cpuid
使用lfence
代替cpuid
; 在 Intel 自永远和启用 Spectre 缓解的 AMD 上,lfence
将序列化指令流,从而用rdtsc
.
请记住,RDTSC 计算参考周期,而不是核心时钟周期。 获取 CPU 周期数?为此以及有关 RDTSC 的更多信息。
您没有cpuid
或lfence
在您的测量间隔内。但是您确实rdtscp
在测量间隔内拥有自己。背靠背rdtscp
并不快,如果您在没有预热 CPU 的情况下运行,64 个参考周期听起来完全合理。空闲时钟速度通常比参考周期慢很多;1 个参考周期等于或接近英特尔 CPU 上的“标贴”频率,例如最大非涡轮持续频率。例如,“4GHz”Skylake CPU 上的 4008 MHz。
这不是您为单个指令计时的方式
重要的是另一条指令可以使用结果之前的延迟,而不是直到它从无序后端完全退出之前的延迟。 RDTSC 可用于对一条加载或一条存储指令所需时间的相对变化进行计时,但开销意味着您不会得到一个好的绝对时间。
不过,您可以尝试减去测量开销。例如clflush 通过 C 函数使缓存行无效。另请参阅后续内容:使用时间戳计数器和 clock_gettime 进行缓存未命中和使用时间戳计数器进行内存延迟测量。
这是我通常用来分析 short block 指令的延迟或吞吐量(以及 uops 融合和未融合域)的方法。调整你如何使用它来限制像这里这样的延迟,或者如果你只想测试吞吐量。例如,使用%rep
具有足够不同寄存器的块来隐藏延迟,或者pxor xmm3, xmm3
在一个短块之后使用 a 打破依赖链,让乱序 exec 发挥它的魔力。(只要你在前端没有瓶颈。)
您可能想要使用 NASM 的 smartalign 包或使用 YASM,以避免 ALIGN 指令的单字节 NOP 指令的墙壁。即使在始终支持 long-NOP 的 64 位模式下,NASM 也默认为非常愚蠢的 NOP。
global _start
_start:
mov ecx, 1000000000
; linux static executables start with XMM0..15 already zeroed
align 32 ; just for good measure to avoid uop-cache effects
.loop:
;; LOOP BODY, put whatever you want to time in here
times 4 addsd xmm4, xmm3
dec ecx
jnz .loop
mov eax, 231
xor edi, edi
syscall ; x86-64 Linux sys_exit_group(0)
使用类似这样的单线程序运行它,将其链接到静态可执行文件并使用 配置文件perf stat
,您可以向上箭头并在每次更改源时重新运行:
asm-link
(实际上,我将nasm+ld + 可选的反汇编放入了一个%if
名为代码。如果你想在头脑中测试理论时向后滚动,它就在你的终端上,就在配置文件之前。)
t=testloop; nasm -felf64 -g "$t.asm" && ld "$t.o" -o "$t" && objdump -drwC -Mintel "$t" &&
taskset -c 3 perf stat -etask-clock,context-switches,cpu-migrations,page-faults,cycles,branches,instructions,uops_issued.any,uops_executed.thread -r4 ./"$t"
来自 i7-6700k 在 3.9GHz 的结果(当前perf
有一个辅助列的单位缩放显示错误。它已在上游修复,但 Arch Linux 尚未更新。):
Performance counter stats for './testloop' (4 runs):
4,106.09 msec task-clock # 1.000 CPUs utilized ( +- 0.01% )
17 context-switches # 4.080 M/sec ( +- 5.65% )
0 cpu-migrations # 0.000 K/sec
2 page-faults # 0.487 M/sec
16,012,778,144 cycles # 3900323.504 GHz ( +- 0.01% )
1,001,537,894 branches # 243950284.862 M/sec ( +- 0.00% )
6,008,071,198 instructions # 0.38 insn per cycle ( +- 0.00% )
5,013,366,769 uops_issued.any # 1221134275.667 M/sec ( +- 0.01% )
5,013,217,655 uops_executed.thread # 1221097955.182 M/sec ( +- 0.01% )
4.106283 +- 0.000536 seconds time elapsed ( +- 0.01% )
在我的 i7-6700k (Skylake) 上,addsd
有 4 个周期延迟,0.5c 吞吐量。(即每个时钟 2 个,如果延迟不是瓶颈)。请参阅https://agner.org/optimize/、https://uops.info/和http://instlatx64.atw.hu/。
每个分支 16 个周期 = 每个链 16 个周期 4 addsd
= 4 个周期延迟addsd
,即使对于这个包含少量启动开销和中断开销的测试,Agner Fog 对 4 个周期的测量结果也优于 100 分之一。
选择不同的计数器进行记录。添加:u
, likeinstructions:u
到 perf 甚至只会计算用户空间指令,不包括在中断处理程序期间运行的任何指令。我通常不这样做,所以我可以将开销视为挂钟时间解释的一部分。但如果你这样做,cycles:u
可以与.instructions:u
-r4
运行它 4 次并取平均值,这对于查看是否存在大量运行之间的变化而不是仅仅从 ECX 中的较高值中获得一个平均值很有用。
调整您的初始 ECX 值以使总时间约为 0.1 到 1 秒,这通常已经足够了,尤其是当您的 CPU 非常快速地加速到最大涡轮时(例如,具有硬件 P 状态和相当激进的 energy_performance_preference 的 Skylake)。或禁用涡轮增压的最大非涡轮增压。
但这计入核心时钟周期,而不是参考周期,因此无论 CPU 频率如何变化,它仍然给出相同的结果。(+- 在转换期间停止时钟会产生一些噪音。)