67

假设我们正在尝试使用 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:这是对cpuidthen的调用rdtsccpuid是一个序列化调用。

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列表中的with的调用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

我对第 3 个选项的理解如下:

进行调用__volatile__可防止优化器删除 asm 或将其移动到可能需要 asm 结果(或更改输入)的任何指令中。但是,它仍然可以移动它以进行不相关的操作。所以__volatile__还不够。

告诉编译器内存正在被破坏:: "memory"). clobber 意味着 GCC 不能对"memory"整个 asm 中的内存内容保持不变做出任何假设,因此不会围绕它重新排序。

所以我的问题是:

  • 1:我的理解__volatile__"memory"正确吗?
  • 2:后两个调用做同样的事情吗?
  • 3:使用"memory"看起来比使用另一个序列化指令简单得多。为什么有人会使用第三个选项而不是第二个选项?
4

2 回答 2

50

如评论中所述,编译器屏障处理器屏障之间存在差异。volatile并且memory在 asm 语句中充当编译器屏障,但处理器仍然可以自由地重新排序指令。

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

顺便说一句,虽然cpuid以前用作屏障rdtsc很常见,但从性能角度来看,它也可能非常糟糕,因为虚拟机平台经常捕获和模拟cpuid指令,以便在集群中的多台机器上强加一组通用的 CPU 特性(以确保实时迁移有效)。因此,最好使用内存围栏指令之一。

Linux 内核mfence;rdtsc在 AMD 平台和lfence;rdtscIntel 上使用。如果您不想费心区分这些,mfence;rdtsc可以同时使用两者,尽管它mfencelfence.

编辑 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

于 2012-09-28T06:41:54.323 回答
6

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

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 本身之前执行。

于 2013-01-08T11:44:10.197 回答