假设我们正在尝试使用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: 这是在clobber列表中使用 memory
调用 rdtsc
,这会阻止重新排序
__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
我对第三种选择的理解如下:
进行调用 __volatile__
会阻止优化器删除asm或将其移动到任何可能需要asm结果(或更改输入)的指令中 . 但是,它仍然可以针对不相关的操作进行移动 . 所以 __volatile__
还不够 .
告诉编译器内存正在被破坏: : "memory")
. "memory"
clobber意味着GCC不能对asm中的内存内容保持相同的任何假设,因此不会对其进行重新排序 .
所以我的问题是:
-
1:我对
__volatile__
和"memory"
的理解是否正确? -
2:接下来的两个电话会做同样的事情吗?
-
3:使用
"memory"
看起来比使用其他序列化指令简单得多 . 为什么有人会在第二个选项中使用第三个选项?
2 回答
正如评论中所提到的,编译器障碍和处理器障碍之间存在差异 . asm语句中的
volatile
和memory
充当编译器障碍,但处理器仍然可以自由重新排序指令 .处理器屏障是必须明确给出的特殊指令,例如
rdtscp, cpuid
,记忆围栏说明(mfence, lfence,
...)等顺便说一下,虽然在
rdtsc
之前使用cpuid
作为屏障是常见的,但从性能角度来看它也可能非常糟糕,因为虚拟机平台经常捕获并模拟cpuid
指令,以便在多台机器上强加一组通用的CPU功能在群集中(以确保实时迁移工作) . 因此,最好使用其中一个内存栅栏指令 .Linux内核在AMD平台上使用
mfence;rdtsc
,在Intel上使用lfence;rdtsc
. 如果你不想打扰区分它们,mfence;rdtsc
同时适用于两者,虽然它稍慢,因为mfence
是一个比lfence
更强的屏障 .您可以使用它,如下所示:
在上面的代码中,第一个CPUID调用实现了一个屏障,以避免在RDTSC指令之上和之下执行指令的无序执行 . 使用此方法,我们可以避免在读取实时寄存器之间调用CPUID指令
然后,第一个RDTSC读取时间戳寄存器,并将值存储在存储器中 . 然后执行我们要测量的代码 . RDTSCP指令第二次读取时间戳寄存器,并保证完成我们想要测量的所有代码的执行 . 之后的两个“mov”指令将edx和eax寄存器值存储到存储器中 . 最后,CPUID调用保证再次实现屏障,以便之后的任何指令都不可能在CPUID本身之前执行 .