RDTSCP与RDTSC + CPUID

时间:2014-12-29 17:15:11

标签: c assembly linux-kernel x86

我正在做一些Linux内核时序,特别是在中断处理路径中。我一直在使用RDTSC进行计时,但是我最近得知它并不一定准确,因为指令可能会发生故障。

然后我尝试了:

  1. RDTSC + CPUID(在此处以相反顺序)刷新管道,并且由于超级调用,在虚拟机(我的工作环境)上产生高达60倍的开销(!)等等。无论是否启用了HW Virtualization,都可以使用此功能。

  2. 最近我遇到了RDTSCP *指令,它似乎做了RDTSC + CPUID所做的事情,但更高效,因为它是一个较新的指令 - 相对而言只有1.5x-2x的开销。

  3. 我的问题: RDTSCP 真正准确地作为衡量标准,是否是正确的计时方式?

    另外要明确一点,我的时间基本上就是这样,内部:

    • 保存当前循环计数器值
    • 执行一种基准测试(即:磁盘,网络)
    • 将当前和上一个周期计数器的增量添加到累加器值并按单个中断递增计数器
    • 最后,将delta / accumulator除以中断次数,得到每次中断的平均周期成本。

    * http://www.intel.de/content/dam/www/public/us/en/documents/white-papers/ia-32-ia-64-benchmark-code-execution-paper.pdf第27页

4 个答案:

答案 0 :(得分:13)

this stackoverflow thread提供了有关您从cpuid指令中看到的开销的完整讨论。使用rdtsc时,需要使用cpuid来确保执行管道中没有其他指令。 rdtscp指令本质上刷新了管道。 (引用的SO线程也讨论了这些重点,但我在这里解决了它们,因为它们也是你问题的一部分。)

你只需要"需要"如果您的处理器不支持rdtscp,请使用cpuid + rdtsc。否则,rdtscp就是您想要的,并且会准确地为您提供您所需的信息。

这两个指令都为您提供了一个64位,单调递增的计数器,表示处理器上的周期数。如果这是你的模式:

uint64_t s, e;
s = rdtscp();
do_interrupt();
e = rdtscp();

atomic_add(e - s, &acc);
atomic_add(1, &counter);

根据您的阅读发生位置,您的平均测量结果可能仍然有一个。例如:

   T1                              T2
t0 atomic_add(e - s, &acc);
t1                                 a = atomic_read(&acc);
t2                                 c = atomic_read(&counter);
t3 atomic_add(1, &counter);
t4                                 avg = a / c;

目前还不清楚" [a]结束"引用可能以这种方式竞赛的时间。如果是这样,您可能希望计算与delta相符的移动平均线或移动平均线。

侧面分:

  1. 如果你确实使用cpuid + rdtsc,你需要减去cpuid指令的成本,这可能很难确定你是否在VM中(取决于VM如何实现该指令)。这就是为什么你应该坚持使用rdtscp。
  2. 在循环中执行rdtscp通常是个坏主意。我经常看到像
  3. 这样的微基准测试

    -

    for (int i = 0; i < SOME_LARGEISH_NUMBER; i++) {
       s = rdtscp();
       loop_body();
       e = rdtscp();
       acc += e - s;
    }
    
    printf("%"PRIu64"\n", (acc / SOME_LARGEISH_NUMBER / CLOCK_SPEED));
    

    虽然这可以让你对loop_body()中的任何内容的循环中的整体性能有所了解,但它会破坏流水线等处理器优化。在微基准测试中,处理器在循环中可以很好地进行分支预测,因此测量循环开销很好。按照上面显示的方式执行操作也很糟糕,因为每次循环迭代最终会有2个管道停顿。因此:

    s = rdtscp();
    for (int i = 0; i < SOME_LARGEISH_NUMBER; i++) {
       loop_body();
    }
    e = rdtscp();
    printf("%"PRIu64"\n", ((e-s) / SOME_LARGEISH_NUMBER / CLOCK_SPEED));
    

    就您在现实生活中所看到的内容与之前的基准测试所告诉您的内容而言,效率更高,可能更准确。

答案 1 :(得分:6)

  

RDTSCP是否真正准确地作为测量点,并且它是正确的&#34;做时间的方式?

现代x86 CPU可以动态调整频率以通过时钟控制(例如英特尔的SpeedStep)来节省功耗,并通过超频提升重负载性能(例如英特尔的Turbo Boost)。然而,这些现代处理器上的时间戳计数器以恒定速率计数(例如,在Linux&#39; s / proc / cpuinfo中查找&#34; constant_tsc&#34;标志)。

所以问题的答案取决于你真正想知道的。除非禁用动态频率缩放(例如,在BIOS中),否则不再依赖时间戳计数器来确定已经过的循环数。但是,仍然可以依靠时间戳计数器来确定已经过去的时间(小心谨慎 - 但我在C中使用clock_gettime - 请参阅我的答案的结尾)。

对我的矩阵乘法代码进行基准测试,并将其与理论上的最佳值进行比较,我需要知道经过的时间和经过的周期(或者更确切地说是测试期间的有效频率)。

让我提出三种不同的方法来确定经过的周期数。

  1. 在BIOS中禁用动态频率缩放并使用时间戳计数器。
  2. 对于Intel处理器,请求性能监视器计数器中的core clock cycles
  3. Measure the frequency under load
  4. 第一种方法是最可靠但它需要访问BIOS并影响你运行的其他所有内容的性能(当我在i5-4250U上禁用动态频率调整时,它以1.3 GHz的恒定速度运行,而不是2.6的基数千兆赫)。仅为了进行基准测试而更改BIOS也很不方便。

    当您不想禁用动态频率范围和/或您无法进行物理访问的系统时,第二种方法非常有用。但是,性能监视器计数器需要特权指令,只有内核或设备驱动程序才能访问。

    第三种方法对于您没有物理访问权限且没有特权访问权限的系统非常有用。这是我在实践中最常用的方法。它原则上是最不可靠的,但在实践中它与第二种方法一样可靠。

    以下是我如何用C确定经过的时间(以秒为单位)。

    #define TIMER_TYPE CLOCK_REALTIME
    
    timespec time1, time2;
    clock_gettime(TIMER_TYPE, &time1);
    foo();
    clock_gettime(TIMER_TYPE, &time2);
    double dtime = time_diff(time1,time2);
    
    double time_diff(timespec start, timespec end)
    {
        timespec temp;
        if ((end.tv_nsec-start.tv_nsec)<0) {
            temp.tv_sec = end.tv_sec-start.tv_sec-1;
            temp.tv_nsec = 1000000000+end.tv_nsec-start.tv_nsec;
        } else {
            temp.tv_sec = end.tv_sec-start.tv_sec;
            temp.tv_nsec = end.tv_nsec-start.tv_nsec;
        }
        return (double)temp.tv_sec +  (double)temp.tv_nsec*1E-9;
    }
    

答案 2 :(得分:4)

以下代码将确保rdstcp在恰当的时间启动。 RDTSCP无法执行得太早,但可以执行到最晚,因为CPU可以在rdtscp之前移动指令以执行它。

为了防止这种情况,我们根据rdstcp将其输出放在edx中的事实创建了一个错误依赖链:eax

rdtscp       ;rdstcp is read serialized, it will not execute too early.
;also ensure it does not execute too late
mov r8,rdx   ;rdtscp changes rdx and rax, force dependency chain on rdx
xor r8,rbx   ;push rbx, do not allow push rbx to execute OoO
xor rbx,rdx  ;rbx=r8
xor rbx,r8   ;rbx = 0
push rdx
push rax
mov rax,rbx  ;rax = 0, but in a way that excludes OoO execution.
cpuid
pop rax
pop rdx
mov rbx,r8
xor rbx,rdx  ;restore rbx

请注意,即使此时间精确到一个周期 您仍然需要多次运行样本并采用那些多次运行的最低时间才能获得实际运行时间。

答案 3 :(得分:3)

2010年英特尔论文How to Benchmark Code Execution Times on Intel ® IA-32 and IA-64 Instruction Set Architectures的建议将RDTSC / RDTSCP与CPUID结合使用时,可以认为已经过时。

当前的英特尔参考文档建议将防护说明作为CPUID的更有效替代方法:

  

请注意,SFENCE,LFENCE和MFENCE指令提供了一种更有效的控制内存的方法   比CPUID指令更重要。

Intel® 64 and IA-32 Architectures Software Developer’s Manual: Volume 3, Section 8.2.5, September 2016

  

如果软件要求仅在执行了所有先前的指令并且所有先前的加载和存储在全局可见之后才执行RDTSC,则它可以在RDTSC之前立即执行序列MFENCE; LFENCE。

Intel RDTSC

因此,要获取TSC起始值,请执行以下指令序列:

mfence
lfence
rdtsc
shl     rdx, 0x20
or      rax, rdx

在基准测试结束时,获得TSC停止值:

rdtscp
lfence
shl     rdx, 0x20
or      rax, rdx

请注意,与CPUID相比,lfence指令不会破坏任何寄存器,因此在执行序列化指令之前不必抢救EDX:EAX寄存器。

相关文档摘要:

  

如果软件要求在执行任何后续指令(包括任何内存访问)之前先执行RDTSCP,则它可以在RDTSCP之后立即执行LFENCE。   (Intel RDTSCP

作为如何将其集成到C程序中的示例,另请参见my GCC inline assembler implementations of the above operations