NASM中的RDTSCP总是返回相同的值(计时单条指令)

5
我正在使用NASM中的RDTSC和RDTSCP来测量各种汇编语言指令的机器周期,以帮助优化。我阅读了Intel(2010年9月)的Gabriele Paoloni的“How to Benchmark Code Execution Times on Intel IA-32 and IA-64 Instruction Set Architectures”以及其他网络资源(其中大部分是C语言示例)。
使用以下代码(从C语言转换而来),我测试各种指令,但是RDTSCP总是返回在RDX中为零,在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。

我的问题如下:

  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

我的rax寄存器中得到了64,但这听起来好像需要太多的周期。


cpuid 是一条序列化指令,用于等待所有先前的指令完成,并防止在读取 TSC 之前启动后续指令。 - RTC222
选择一个不会破坏你结果的指令...? - cHao
根据所有资源,包括Paoloni,CPUID是要使用的。如果在最终调用CPUID之前访问rax,则结果是一个非常大的整数。 - RTC222
请注意,我在上面的问题中进行了更新。 - RTC222
5
听着:CPUID 覆盖了 eax、ecx 和 edx 寄存器。因此,当你执行 CPUID 指令时,来自 RDTSCP 的计时信息将被覆盖,而是被一些与时间无关的数字替换,这些数字告诉你有关你的 CPU 类型。 - Nate Eldredge
显示剩余5条评论
1个回答

11

你的第一段代码(导致标题问题)存在缺陷,因为它用EAX、EBX、ECX和EDX中的cpuid结果覆盖了rdtscrdtscp结果。

使用lfence代替cpuid; 在英特尔处理器上自始至终,在启用Spectre缓解的AMD处理器上,lfence将序列化指令流,从而使用rdtsc实现您想要的效果。


请记住,RDTSC计数参考周期,而不是核心时钟周期。 获取CPU周期计数? 了解有关RDTSC的更多信息。

在您的测量间隔内,您没有cpuidlfence。但是您确实在测量间隔内拥有rdtscp本身。连续的rdtscp并不快速,如果您没有预热CPU,则64个参考周期听起来完全合理。空闲时钟速度通常比参考周期慢得多; 1个参考周期等于或接近“贴纸”频率,例如英特尔CPU上的最大非Turbo持续频率,例如Skylake CPU上的4008 MHz。


这不是衡量单个指令时间的方法

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

您可以尝试减去测量开销。例如clflush通过C函数使缓存行无效。请参见后续内容:使用时间戳计数器和clock_gettime进行缓存丢失使用时间戳计数器进行内存延迟测量


这是我通常用来分析短指令块的延迟或吞吐量(以及融合和未融合域)的工具。根据需要调整使用方式,可以像这里一样限制延迟,或者如果只想测试吞吐量,则不需要。例如,使用具有足够不同寄存器的%rep块来隐藏延迟,或在短块后使用pxor xmm3, xmm3打破依赖链,让乱序执行发挥其魔力。(只要不在前端瓶颈)。您可能希望使用NASM的smartalign包或使用YASM,以避免ALIGN指令的大量单字节NOP指令。在64位模式下,即使始终支持长NOP,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进行性能分析,每次更改源代码时都可以向上箭头重新运行:

(实际上,我将nasm+ld + 可选反汇编放入一个名为asm-link的shell脚本中,以节省在不进行性能分析时的输入。反汇编确保您循环中的内容是您想要进行性能分析的内容,特别是如果您的代码中有一些%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存在单位缩放显示错误的bug,已经在上游修复,但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.5个周期的吞吐量。(即如果延迟不是瓶颈,则每个时钟周期为2)。请参见:https://agner.org/optimize/https://uops.info/http://instlatx64.atw.hu/每个分支16个周期=每个4个addsd链路16个周期= addsd的4个周期延迟,甚至在包含一点启动开销和中断开销的测试中也能准确重现Agner Fog的4个周期延迟测量结果。 选择要记录的不同计数器。将:u添加到perf事件中,例如instructions:u仅计算用户空间指令,不包括在中断处理程序运行期间运行的任何指令。我通常不这样做,因此可以看到超时作为解释壁钟时间的一部分。但是,如果您这样做,cycles:uinstructions:u非常接近。 -r4运行4次并平均,这对于查看运行之间的差异是否很大而不是从ECX中较高值获得一个平均值非常有用。
调整初始ECX值,使总时间约为0.1至1秒,通常已足够,特别是如果您的CPU非常快地达到最大睿频(例如具有硬件P状态和相当激进的energy_performance_preference的Skylake)或关闭睿频时的最大非睿频。
但是,这计算以核心时钟周期而不是参考时钟周期为单位,因此仍会在CPU频率更改时给出相同的结果。(+-一些噪音来自在过渡期间停止时钟。)

感谢您提供详细的答案。我使用的是Windows系统,因此我正在进行一些额外的研究,以便在需要时发布跟进问题。 - RTC222
1
@RTC222:那么请调用Windows退出函数,而不是进行Linux系统调用。并且在Windows上使用您选择的PMU计数器分析器,而不是Linux的perf。或者只需查看已知CPU频率的挂钟时间,如果您可以确保这一点(例如通过禁用Turbo)。通常延迟以整个周期为单位,因此非常容易。吞吐量可能会更难,如果存在奇怪的影响或前端复杂性。 - Peter Cordes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接