假设我们正在尝试使用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结果(或更改输入)的任何指令之前。但是,它仍然可以相对于不相关的操作移动。因此,__volatile__
还不够。
告诉编译器内存被破坏了:: "memory")
。 "memory"
表示GCC不能对asm中的内存内容做出任何假设,因此不会重新排序它。
所以我的问题是:
- 1:我对
__volatile__
和"memory"
的理解正确吗? - 2:后两个调用是否执行相同的操作?
- 3:使用
"memory"
比使用另一个序列化指令简单得多。为什么有人会选择第三种选项而不是第二种选项?
memory
不会防止处理器重新排序指令吗?memory
不像内存栅栏一样起作用吗? - Steve Lorimermemory
只是针对gcc发出的,而生成的机器码并不会将其暴露给处理器? - Steve Lorimermovntdq
,以便在多线程环境中使用。在Intel / AMD处理器上,大多数情况下您不需要内存栅栏,因为这些处理器默认具有强内存排序。是的,“memory”只影响编译器发出指令的顺序,它不会使编译器发出额外的指令。 - Gunther Piezrdtscp
指令不能防止重排序,它仅确保所有先前的指令已经执行完毕后才会读取计数器的值:RDTSCP指令等待所有先前的指令都执行完毕才会读取计数器。但在执行读操作之前,后续指令可能已经开始执行。
如果你考虑将其用于基准测试等,请参考英特尔的这篇白皮书: http://download.intel.com/embedded/software/IA/324264.pdf(实际上显示你需要使用`rdtsc`+cpuid
和rdtscp
+cpuid
来进行正确的测量)。 - Necrolis