rdtscp,rdtsc:memory和cpuid / rdtsc之间的区别?

时间:2012-09-28 00:07:46

标签: c++ c performance assembly rdtsc

假设我们正在尝试使用tsc进行性能监控,我们希望阻止指令重新排序。

这些是我们的选择:

1: rdtscp是序列化调用。它可以防止对rdtscp的调用进行重新排序。

__asm__ __volatile__("rdtscp; "         // serializing read of tsc
                     "shl $32,%%rdx; "  // shift higher 32 bits stored in rdx up
                     "or %%rdx,%%rax"   // and or onto rax
                     : "=a"(tsc)        // output to tsc variable
                     :
                     : "%rcx", "%rdx"); // rcx and rdx are clobbered

但是,rdtscp仅适用于较新的CPU。所以在这种情况下我们必须使用rdtsc。但rdtsc是非序列化的,因此单独使用它不会阻止CPU重新排序。

因此,我们可以使用这两个选项中的任何一个来阻止重新排序:

2:这是对cpuid然后rdtsc的来电。 cpuid是一个序列化调用。

volatile int dont_remove __attribute__((unused)); // volatile to stop optimizing
unsigned tmp;
__cpuid(0, tmp, tmp, tmp, tmp);                   // cpuid is a serialising call
dont_remove = tmp;                                // prevent optimizing out cpuid

__asm__ __volatile__("rdtsc; "          // read of tsc
                     "shl $32,%%rdx; "  // shift higher 32 bits stored in rdx up
                     "or %%rdx,%%rax"   // and or onto rax
                     : "=a"(tsc)        // output to tsc
                     :
                     : "%rcx", "%rdx"); // rcx and rdx are clobbered

3:这是在{@ 1}}中使用rdtsc调用clobber列表,这会阻止重新排序

memory

我对第三种选择的理解如下:

拨打电话__asm__ __volatile__("rdtsc; " // read of tsc "shl $32,%%rdx; " // shift higher 32 bits stored in rdx up "or %%rdx,%%rax" // and or onto rax : "=a"(tsc) // output to tsc : : "%rcx", "%rdx", "memory"); // rcx and rdx are clobbered // memory to prevent reordering 会阻止优化器移除asm或将其移动到任何可能需要asm结果(或更改输入)的指令中。但是,它仍然可以针对不相关的操作进行移动。所以__volatile__还不够。

告诉编译器内存正在被破坏:__volatile__: "memory") clobber意味着GCC不能对asm中的内存内容保持相同的任何假设,因此不会对其进行重新排序。

所以我的问题是:

  • 1:我对"memory"__volatile__的理解是否正确?
  • 2:接下来的两个电话会做同样的事吗?
  • 3:使用"memory"看起来比使用其他序列化指令简单得多。为什么有人会在第二个选项中使用第三个选项?

2 个答案:

答案 0 :(得分:41)

正如评论中所提到的,编译器障碍处理器障碍之间存在差异。 asm语句中的volatilememory充当编译器障碍,但处理器仍然可以自由重新排序指令。

处理器屏障是必须明确给出的特殊指令,例如: rdtscp, cpuid,记忆围栏说明(mfence, lfence, ...)等。

顺便说一句,虽然在cpuid之前使用rdtsc作为障碍是常见的,但从性能角度来看,它也可能非常糟糕,因为虚拟机平台经常陷阱并模拟{{1} }指令,以便在群集中的多台计算机上强加一组通用的CPU功能(以确保实时迁移工作)。因此,最好使用其中一个内存栅栏指令。

Linux内核在AMD平台上使用cpuid,在英特尔上使用mfence;rdtsc。如果您不想费心区分这两者,lfence;rdtsc同时适用于两者,虽然它稍慢,因为mfence;rdtscmfence更强屏障。

答案 1 :(得分:5)

您可以使用它,如下所示:

asm volatile (
"CPUID\n\t"/*serialize*/
"RDTSC\n\t"/*read the clock*/
"mov %%edx, %0\n\t"
"mov %%eax, %1\n\t": "=r" (cycles_high), "=r"
(cycles_low):: "%rax", "%rbx", "%rcx", "%rdx");
/*
Call the function to benchmark
*/
asm volatile (
"RDTSCP\n\t"/*read the clock*/
"mov %%edx, %0\n\t"
"mov %%eax, %1\n\t"
"CPUID\n\t": "=r" (cycles_high1), "=r"
(cycles_low1):: "%rax", "%rbx", "%rcx", "%rdx");

在上面的代码中,第一个CPUID调用实现了一个屏障,以避免在RDTSC指令之上和之下执行指令的无序执行。使用此方法,我们可以避免在读取实时寄存器之间调用CPUID指令

然后,第一个RDTSC读取时间戳寄存器,并将值存储在中 记忆。然后执行我们要测量的代码。 RDTSCP指令第二次读取时间戳寄存器,并保证完成我们想要测量的所有代码的执行。随后的两个“mov”指令将edx和eax寄存器值存储到存储器中。最后,一个CPUID调用保证再次实现一个屏障,以便之后的任何指令都不可能在CPUID本身之前执行。