RDTSCP与RDTSC + CPUID

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

然后我尝试了:

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

  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页

您可以在此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); 

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

  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; 

目前还不清楚“结束”是否指的是可以以这种方式竞争的时间。 如果是这样,您可能希望计算与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)); 

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

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

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

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

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

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

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

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

  1. 禁用BIOS中的动态频率缩放并使用时间戳计数器。
  2. 对于Intel处理器,请求性能监视器计数器的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; } 

以下代码将确保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 

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