RDTSCP与RDTSC + CPUID

Ric*_*ner 15 c x86 assembly linux-kernel

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

然后我尝试了:

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

  2. 最近我遇到了RDTSCP*指令,它看起来像RDTSC + CPUID那样做,但更高效,因为它是一个较新的指令 - 相对而言只有1.5x-2x的开销.

我的问题: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页

dho*_*dho 13

您可以在此stackoverflow线程中详细讨论您从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);
Run Code Online (Sandbox Code Playgroud)

根据读取的位置,您的平均测量结果可能仍然是一个接一个.例如:

   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;
Run Code Online (Sandbox Code Playgroud)

目前还不清楚"结束"是否引用了一种可能以这种方式竞争的时间.如果是这样,您可能希望计算与您的delta一致的移动平均线或移动平均线.

侧点:

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

-

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));
Run Code Online (Sandbox Code Playgroud)

虽然这样可以让您对循环中的整体性能有所了解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));
Run Code Online (Sandbox Code Playgroud)

就你在真实生活中看到的内容与之前的基准测试所告诉你的内容而言,它会更高效,也可能更准确.


Z b*_*son 6

RDTSCP作为测量点真的准确吗,它是做出时间的"正确"方法吗?

现代x86 CPU可以通过计时(例如Intel的SpeedStep)动态调整频率以节省功耗,并通过超频提升重负载性能(例如Intel的Turbo Boost).然而,这些现代处理器上的时间戳计数器以恒定速率计数(例如,在Linux的/ proc/cpuinfo中查找"constant_tsc"标志).

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

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

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

  1. 禁用BIOS中的动态频率缩放并使用时间戳计数器.
  2. 对于英特尔处理器,请求core clock cycles性能监视器计数器.
  3. 测量负载下的频率.

第一种方法是最可靠的,但它需要访问BIOS并影响您运行的其他所有内容的性能(当我在i5-4250U上禁用动态频率调整时,它运行在恒定的1.3 GHz而不是2.6 GHz的基础上).仅为基准测试更改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;
}
Run Code Online (Sandbox Code Playgroud)


max*_*zig 5

2010 年的英特尔论文如何对英特尔® IA-32 和 IA-64 指令集架构上的代码执行时间进行基准测试,就其将 RDTSC/RDTSCP 与 CPUID 相结合的建议而言,可能被认为已经过时。

当前的 Intel 参考文档建议将防护指令作为 CPUID 的更有效替代方案:

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

英特尔® 64 位和 IA-32 架构软件开发人员手册:第 3 卷,第 8.2.5 节,2016 年 9 月

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

英特尔 RDTSC

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

mfence
lfence
rdtsc
shl     rdx, 0x20
or      rax, rdx
Run Code Online (Sandbox Code Playgroud)

在基准测试结束时,获取 TSC 止损值:

rdtscp
lfence
shl     rdx, 0x20
or      rax, rdx
Run Code Online (Sandbox Code Playgroud)

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

相关文档片段:

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

作为如何将其集成到 C 程序的示例,另请参阅我的上述操作的 GCC 内联汇编器实现