NASM中的RDTSCP始终返回相同的值

RTC*_*222 3 optimization x86-64 nasm rdtsc windows64

我正在NASM中使用RDTSC和RDTSCP测量各种汇编语言指令的机器周期,以帮助优化。

我读了Intel的Gabriele Paoloni撰写的“如何在Intel IA-32和IA-64指令集体系结构上对代码执行时间进行基准测试”(2010年9月)和其他Web资源(其中大多数是C语言中的示例)。

使用下面的代码(从C转换),我测试了各种指令,但RDTSCP在RDX中始终返回零,在RAX中始终返回7。我首先认为7是周期数,但显然并非所有指令都需要7个周期。

rdtsc
cpuid
addsd xmm14,xmm1 ; Instruction to time
rdtscp
cpuid
Run Code Online (Sandbox Code Playgroud)

返回7,这并不奇怪,因为在某些体系结构上,添加了7个周期(包括延迟)。前两个指令(根据某些情况)可以颠倒,先是cpuid,然后是rdtsc,但这在这里没有什么区别。

当我将指令更改为2周期指令时:

rdtsc
cpuid
add rcx,rdx ; Instruction to time
rdtscp
cpuid
Run Code Online (Sandbox Code Playgroud)

这还会在rax中返回7,在rdx中返回零。

所以我的问题是:

  1. 如何访问和解释RDX:RAX中返回的值?

  2. 为什么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
Run Code Online (Sandbox Code Playgroud)

我的rax达到了64,但这听起来像是周期太多。

Pet*_*des 7

您的第一个代码(导致标题问题)有问题,因为它会覆盖EAX,EBX,ECX和EDX中的rdtscrdtscp结果cpuid

使用lfence代替cpuid ; 自从永远在Intel上启用AMD并启用了Spectre缓解功能后,AMD lfence就会对指令流进行序列化,从而根据需要执行操作rdtsc


请记住,RDTSC会计算参考周期,而不是内核时钟周期。 获取CPU周期计数?为此以及有关RDTSC的更多信息。

您没有测量间隔cpuidlfence在测量间隔之内。但是你做的rdtscp本身的测量间隔。背对背的rdtscp速度并不快,如果不预热CPU就可以运行64个参考周期,这完全是合理的。空闲时钟速度通常比参考周期慢很多;1个参考周期等于或接近Intel CPU上的“贴纸”频率,例如最大非涡轮持续频率。例如“ 4GHz” Skylake CPU上的4008 MHz。


这不是您安排单个指令的时间

重要的是在另一条指令可以使用结果之前的延迟,而不是直到它从无序后端完全退出之前的延迟。 RDTSC有助于计时一条加载或一条存储指令花费的时间的相对变化,但是开销意味着您不会获得良好的绝对时间。

但是,您可以尝试减少测量开销。例如clflush通过C函数使缓存行无效。另请参阅后续内容:使用时间戳记计数器和clock_gettime进行缓存未命中,使用时间戳记计数器测量内存延迟


这就是我通常用来描述短块指令的延迟或吞吐量(以及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)
Run Code Online (Sandbox Code Playgroud)

使用这样的单行代码运行该文件,将其链接到一个静态可执行文件并使用进行配置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"
Run Code Online (Sandbox Code Playgroud)

来自3.9 GHz的i7-6700k的结果(当前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% )
Run Code Online (Sandbox Code Playgroud)

在我的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分之一以上。

选择不同的计数器进行记录。甚至向性能中添加:uinstructions:u只会计算用户空间指令,不包括在中断处理程序中运行的指令。我通常不这样做,因此我可以将其作为挂钟时间说明的一部分。但是,如果你这样做,cycles:u可以匹配非常密切instructions:u

-r4 将其运行4次并取平均值,这对于查看是否存在大量的运行差异很有用,而不是仅从ECX中的较高值中获得一个平均值。

调整您的初始ECX值,使总时间大约为0.1到1秒,这通常就足够了,尤其是当您的CPU非常迅速地加速到最大加速时(例如具有硬件P状态和相当激进的energy_performance_preference的Skylake)。或最大非涡轮增压,禁用涡轮增压。

但这计入核心时钟周期,而不是参考周期,因此,无论CPU频率如何变化,它仍然给出相同的结果。(+-过渡期间停止时钟会产生一些噪音。)