假设我们正在尝试使用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中的内存内容保持相同的任何假设,因此不会对其进行重新排序。
所以我的问题是:
"memory"
和__volatile__
的理解是否正确?"memory"
看起来比使用其他序列化指令简单得多。为什么有人会在第二个选项中使用第三个选项?答案 0 :(得分:41)
正如评论中所提到的,编译器障碍与处理器障碍之间存在差异。 asm语句中的volatile
和memory
充当编译器障碍,但处理器仍然可以自由重新排序指令。
处理器屏障是必须明确给出的特殊指令,例如: rdtscp, cpuid
,记忆围栏说明(mfence, lfence,
...)等。
顺便说一句,虽然在cpuid
之前使用rdtsc
作为障碍是常见的,但从性能角度来看,它也可能非常糟糕,因为虚拟机平台经常陷阱并模拟{{1} }指令,以便在群集中的多台计算机上强加一组通用的CPU功能(以确保实时迁移工作)。因此,最好使用其中一个内存栅栏指令。
Linux内核在AMD平台上使用cpuid
,在英特尔上使用mfence;rdtsc
。如果您不想费心区分这两者,lfence;rdtsc
同时适用于两者,虽然它稍慢,因为mfence;rdtsc
比mfence
更强屏障。
答案 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本身之前执行。