rdtscp,rdtsc:memory和cpuid/rdtsc之间的区别?

Ste*_*mer 61 c c++ performance assembly 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
Run Code Online (Sandbox Code Playgroud)

但是,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
Run Code Online (Sandbox Code Playgroud)

3:这是在clobber列表中调用rdtscwith 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
Run Code Online (Sandbox Code Playgroud)

我对第三种选择的理解如下:

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

告诉编译器内存正在被破坏:: "memory").该"memory"撞意味着GCC不能对存储器的内容保持不变跨越ASM,因而不会在其周围重新排序任何假设.

所以我的问题是:

  • 1:我的理解__volatile__"memory"正确吗?
  • 2:接下来的两个电话会做同样的事情吗?
  • 3:使用"memory"看起来比使用另一个序列化指令简单得多.为什么有人会在第二个选项中使用第三个选项?

jan*_*neb 44

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

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

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

Linux内核mfence;rdtsc在AMD平台和lfence;rdtsc英特尔上使用.如果你不想打扰区分它们,那么mfence;rdtsc两者都有效,虽然它稍微慢一点,因为mfence它比一个更强的屏障lfence.

  • `cpuid; rdtsc`不是关于内存栅栏,而是关于序列化指令流.通常它用于基准测试目的,以确保重新排序缓冲区/保留站中没有"旧"指令.然后减去`cpuid`的执行时间(这很长,我记得> 200个循环).如果结果更"精确",这种方式对我来说不是很清楚,我试验过和没有,差异似乎不是测量的自然误差,即使在没有其他任何运行的单用户模式下也是如此. (6认同)
  • @hirschhornsalz:根据git提交日志,AMD和英特尔确认m/lfence将在当前可用的CPU上序列化rdtsc.我想Andi Kleen可以提供更多详细信息,如果你有兴趣并问他的话. (5认同)
  • 在 Intel 上使用 `lfence`,在 AMD 上使用 `mfence` 可能很重要;任何关于“更强屏障”的论点都是完全不适用的,因为我们谈论的是指令流和额外的微架构效果,*不是*有据可查的内存排序效果。例如,LFENCE 并没有在 AMD 上完全序列化:它具有每时钟 4 个吞吐量的推土机系列/锐龙!也许它确实序列化了 `rdtsc` 而不是它自己或其他一些指令?或者更有可能它在 AMD 上非常便宜,因为它们的内存排序实现的工作方式不同。 (2认同)

小智 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");
Run Code Online (Sandbox Code Playgroud)

在上面的代码中,第一个CPUID调用实现了一个屏障,以避免在RDTSC指令之上和之下执行指令的无序执行.使用此方法,我们可以避免在读取实时寄存器之间调用CPUID指令

然后,第一个RDTSC读取时间戳寄存器,并将值存储在存储器中.然后执行我们要测量的代码.RDTSCP指令第二次读取时间戳寄存器,并保证完成我们想要测量的所有代码的执行.随后的两个"mov"指令将edx和eax寄存器值存储到存储器中.最后,CPUID调用保证再次实现屏障,以便之后的任何指令都不可能在CPUID本身之前执行.

  • 嗨,似乎你从Gabriele Paolinis白皮书"如何在英特尔®IA-32和IA-64指令集架构上基准代码执行时间"中复制了这个答案(你错过了一个换行符).你在没有给予作者信用的情况下使用别人的作品.为什么不添加归因? (15认同)
  • 如果您打算使用自己的内联asm而不是内置/内在,至少编写高效的内联asm,它使用约束来告诉编译器要查看哪些寄存器,而不是使用`mov`指令. (3认同)

归档时间:

查看次数:

26266 次

最近记录:

5 年,11 月 前