unsigned int lo = 0;
unsigned int hi = 0;
__asm__ __volatile__ (
"mfence;rdtsc" : "=a"(lo), "=d"(hi) : : "memory"
);
上面代码中的 mfence
,是否有必要?
根据我的测试,找不到cpu重新排序。
测试代码片段包含在下面。
inline uint64_t clock_cycles() {
unsigned int lo = 0;
unsigned int hi = 0;
__asm__ __volatile__ (
"rdtsc" : "=a"(lo), "=d"(hi)
);
return ((uint64_t)hi << 32) | lo;
}
unsigned t1 = clock_cycles();
unsigned t2 = clock_cycles();
assert(t2 > t1);
答案 0 :(得分:5)
使用rdtsc
执行合理测量所需的是序列化指令。
众所周知,很多人在 cpuid
之前使用rdtsc
。
rdtsc
需要从上面的 和下面的序列化(读取:必须退出之前的所有指令,并且必须在测试代码启动之前退出)。
不幸的是,第二个条件经常被忽略,因为cpuid
对于这个任务来说是一个非常糟糕的选择(它会破坏rdtsc
的输出。)
在寻找替代方案时,人们会认为在他们的名字中有“围栏”的指令会这样做,但这也是不真实的。直接来自英特尔:
MFENCE不会序列化指令流。
几乎序列化的指令将在以前的商店不需要完成的任何测量中执行lfence
。
简单地说,lfence
确保在任何先前的指令在本地完成之前没有新的指令开始。见this answer of mine for a more detailed explanation on locality。
它也不会像mfence
那样耗尽存储缓冲区,也不会像cpuid
那样破坏寄存器。
所以lfence / rdtsc / lfence
是一个比mfence / rdtsc
更精心设计的指令序列,其中mfence
几乎没用,除非您明确要求在测试开始/结束之前完成之前的商店(但不是在执行rdstc
之前!)。
如果您检测重新排序的测试是assert(t2 > t1)
,那么我相信您将不会测试任何内容
退出return
以及可能会或可能不会阻止CPU及时看到第二个rdtsc
进行重新排序的调用,不太可能(尽管可能!)CPU将重新排序两个{{ 1}}即使一个人正好接着另一个人。
想象一下,我们有一个rdtsc
完全,如rdtsc2
但写rdtsc
1 。
执行
ecx:ebx
很有可能是rdtsc
rdtsc2
,因为在<{1}}之前,CPU 无理由执行ecx:ebx > edx:eax
。
重新排序并不意味着随机排序,它意味着寻找其他指令如果当前的指令无法执行。
但是rdtsc2
不依赖于任何先前的指令,所以当遇到OoO核心时,它不可能被延迟。
然而,特殊的内部微观建筑细节可能会使我的论文无效,因此我之前的陈述中的可能是这个词。
1 我们不需要这个改变的指令:寄存器重命名会这样做,但如果你不熟悉它,这将有所帮助。
答案 1 :(得分:3)
mfence 是强制在CPU中进行序列化的。
通常你会在那里找到 cpuid (这也是序列化指令)。
英特尔手册中有关使用 rdtsc 的报价会更清楚
从Intel Pentium处理器开始,大多数Intel CPU都支持 无序执行代码。目的是优化 由于不同的指令延迟而受到处罚。不幸 此功能不保证时间顺序 单个编译的C指令将遵循该序列 指令自己写在源C文件中。当我们打电话 在RDTSC指令中,我们假装该指令将是 正好在代码开始和结束时执行 测量(即,我们不想测量执行的编译代码 在RDTSC之外调用或在调用之间执行 他们自己)。 解决方案是之前调用序列化指令 称之为RDTSC。序列化指令是指令 这会强制CPU完成C的每个前面的指令 继续执行程序之前的代码。通过这样做,我们保证 只会测量正在测量的代码 在RDTSC调用之间,并且不会执行该代码的任何部分 在电话之外。
TL; DR版本 - 在rdtsc之前没有序列化指令你不知道该指令何时开始执行,使得测量结果可能不正确。
提示 - 尽可能使用rdtscp。
根据我的测试,找不到cpu重新排序。
仍然不能保证它可能发生 - 这就是为什么原始代码有"memory"
来表示可能的内存崩溃阻止编译器重新排序的原因。