提问人:RTC222 提问时间:2/11/2019 最后编辑:Peter CordesRTC222 更新时间:11/19/2020 访问量:1264
NASM 中的 RDTSCP 始终返回相同的值(对单个指令进行计时)
RDTSCP in NASM always returns the same value (timing a single instruction)
问:
我在 NASM 中使用 RDTSC 和 RDTSCP 来测量各种汇编语言指令的机器周期,以帮助优化。
我阅读了 Gabriele Paoloni 在英特尔(2010 年 9 月)撰写的“如何在英特尔 IA-32 和 IA-64 指令集架构上对代码执行时间进行基准测试”和其他网络资源(其中大部分是 C 语言示例)。
使用下面的代码(从 C 翻译过来),我测试了各种指令,但 RDTSCP 在 RDX 中总是返回 0,在 RAX 中总是返回 7。我最初认为 7 是周期数,但显然并非所有指令都需要 7 个周期。
rdtsc
cpuid
addsd xmm14,xmm1 ; Instruction to time
rdtscp
cpuid
这将返回 7,这并不奇怪,因为在某些架构上,addsd 是 7 个周期,包括延迟。前两个指令(根据某些人的说法)可以颠倒,首先是 cpuid,然后是 rdtsc,但这在这里没有区别。
当我将指令更改为 2 周期指令时:
rdtsc
cpuid
add rcx,rdx ; Instruction to time
rdtscp
cpuid
这也在 rax 中返回 7,在 rdx 中返回 0。
所以我的问题是:
如何访问和解释 RDX:RAX 中返回的值?
为什么 RDX 总是返回零,它应该返回什么?
更新:
如果我将代码更改为:
cpuid
rdtsc
mov [start_time],rax
addsd xmm14,xmm1 ; INSTRUCTION
rdtscp
mov [end_time],rax
cpuid
mov rax,[end_time]
mov rdx,[start_time]
sub rax,rdx
我在 rax 中得到 64,但这听起来像是太多的周期。
答:
您的第一个代码(导致标题问题)是错误的,因为它用 EAX、EBX、ECX 和 EDX 中的 cpuid
结果覆盖了 rdtsc 和 rdtscp
结果。
使用 lfence
代替 cpuid
;在 Intel 上,因为永远和 AMD 启用了 Spectre 缓解,将序列化指令流,从而执行您想要的操作。lfence
rdtsc
请记住,RDTSC 计算的是参考周期,而不是内核时钟周期。 获取 CPU 周期计数?以及有关RDTSC的更多信息。
您没有测量间隔或在测量间隔内。但是您确实在测量间隔内。背靠背并不快,如果您在不预热 CPU 的情况下运行,64 个参考周期听起来完全合理。空闲时钟速度通常比参考周期慢得多;1 参考周期等于或接近英特尔 CPU 上的“贴纸”频率,例如 .max 非睿频持续频率。例如,“4GHz”Skylake CPU 上的 4008 MHz。cpuid
lfence
rdtscp
rdtscp
这不是你给单个指令计时的方式
重要的是另一条指令可以使用结果之前的延迟,而不是从无序后端完全停用之前的延迟。RDTSC 可用于对一个加载或一个存储指令所花费时间的相对变化进行计时,但开销意味着您将无法获得良好的绝对时间。
不过,您可以尝试减去测量开销。例如,clflush 通过 C 函数使缓存行失效。另请参阅后续文章:使用时间戳计数器和clock_gettime进行缓存未命中和使用时间戳计数器测量内存延迟。
这是我通常用来分析短块指令的延迟或吞吐量(以及 uops 融合和未融合域)的方法。调整使用它的方式,以解决延迟的瓶颈,就像这里一样,或者如果你只想测试吞吐量,则不调整。例如,使用具有足够多不同寄存器的块来隐藏延迟,或者在短块之后断开依赖链,让无序执行器发挥其魔力。(只要你不在前端出现瓶颈。%rep
pxor xmm3, xmm3
您可能希望使用 NASM 的 smartalign 包或使用 YASM,以避免 ALIGN 指令出现一大堆单字节 NOP 指令。NASM 默认为非常愚蠢的 NOP,即使在始终支持 long-NOP 的 64 位模式下也是如此。
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
(我实际上将 nasm+ld + 可选的反汇编放入一个名为 的 shell 脚本中,以便在我不分析时节省输入。反汇编可以确保循环中的内容是你想要分析的内容,尤其是当你的代码中有一些东西时。而且,如果您想在测试脑海中的理论时向后滚动,它就在个人资料之前的终端上。asm-link
%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"
在 3.9GHz 下 i7-6700k 的结果(当前二级列有一个单位缩放显示错误。它在上游已修复,但 Arch Linux 尚未更新。perf
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) 上,有 4 个周期的延迟,0.5c 的吞吐量。(即,如果延迟不是瓶颈,则每个时钟 2 个)。请参阅 https://agner.org/optimize/、https://uops.info/ 和 http://instlatx64.atw.hu/。addsd
每个分支 16 个周期 = 每链 16 个周期,每链 4 个 addsd = addsd
的 4 个周期延迟,再现了 Agner Fog 对 4 个
周期的测量结果,即使对于包含少量启动开销和中断开销的测试,也优于 1/100。
选择不同的计数器进行记录。将 、 like 添加到性能中甚至只会计算用户空间指令,不包括在中断处理程序期间运行的任何指令。我通常不会这样做,所以我可以把这种开销看作是对挂钟时间解释的一部分。但如果你这样做了,可以非常接近地匹配.:u
instructions:u
cycles:u
instructions:u
-r4
运行 4 次并取平均值,这对于查看是否存在大量运行间差异非常有用,而不仅仅是从 ECX 中的较高值中获取一个平均值。
调整您的初始 ECX 值,使总时间约为 0.1 到 1 秒,这通常就足够了,尤其是当您的 CPU 非常快地上升到最大睿频时(例如,具有硬件 P 状态和相当激进的 energy_performance_preference 的 Skylake)。或禁用涡轮增压的最大非涡轮增压。
但这以内核时钟周期计入,而不是以参考周期计入,因此无论 CPU 频率如何变化,它仍然给出相同的结果。(+- 在转换期间停止时钟的一些噪音。
评论
perf
评论
cpuid