[英]Difference between rdtscp, rdtsc : memory and cpuid / 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:这是对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
我对第三个选项的理解如下:
调用__volatile__
可以防止优化器删除 asm 或将其移动到任何可能需要 asm 结果(或更改输入)的指令中。 但是,它仍然可以针对不相关的操作移动它。 所以__volatile__
是不够的。
告诉编译器内存正在被破坏: : "memory")
。 "memory"
破坏意味着 GCC 不能对整个 asm 中的内存内容保持不变做出任何假设,因此不会围绕它重新排序。
所以我的问题是:
__volatile__
和"memory"
理解正确吗?"memory"
看起来比使用另一个序列化指令简单得多。 为什么有人会使用第三个选项而不是第二个选项?正如评论中提到的,编译器屏障和处理器屏障之间存在差异。 asm 语句中的volatile
和memory
充当编译器屏障,但处理器仍然可以自由地重新排序指令。
处理器屏障是必须明确给出的特殊指令,例如rdtscp, cpuid
、内存栅栏指令( mfence, lfence,
...)等。
rdtsc
,虽然在rdtsc
之前使用cpuid
作为障碍很常见,但从性能角度来看,它也可能非常糟糕,因为虚拟机平台经常捕获和模拟cpuid
指令,以便在多台机器上强加一组通用的 CPU 功能在集群中(以确保实时迁移有效)。 因此最好使用内存栅栏指令之一。
Linux内核使用mfence;rdtsc
在AMD平台上和lfence;rdtsc
英特尔。 如果您不想费心区分这些, mfence;rdtsc
对两者都适用,尽管它稍微慢一些,因为mfence
是比lfence
更强的屏障。
编辑 2019-11-25 :从 Linux 内核 5.4 开始,lfence 用于在 Intel 和 AMD 上序列化 rdtsc。 请参阅此提交“x86:删除 X86_FEATURE_MFENCE_RDTSC”: https ://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/ ? id = be261ffce6f13229dad50f59c5e491f933d3167f
你可以像下图那样使用它:
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 本身之前执行。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.