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);
使用rdtsc
执行合理测量所需的是序列化指令。
众所周知,很多人在rdtsc
之前使用cpuid
。rdtsc
需要从上面和下面序列化(阅读:它之前的所有指令都必须停用,并且必须在测试代码开始之前停用)。
不幸的是,第二个条件经常被忽略,因为对于这个任务来说cpuid
是一个非常糟糕的选择(它会破坏rdtsc
的输出)。
在寻找替代方案时,人们认为名称中带有"围栏"的指令就可以了,但这也是不正确的。直接来自英特尔:
MFENCE 不序列化指令流。
一条几乎是序列化的指令,可以在以前的商店不需要完成的任何测量中执行lfence
。
简而言之,lfence
确保在任何先前的指令在本地完成之前没有新指令开始。有关地方性的更详细解释,请参阅我的这个答案。
它也不会像mfence
那样耗尽存储缓冲区,也不会像cpuid
那样破坏寄存器。
因此,lfence / rdtsc / lfence
是一个比mfence / rdtsc
更好的指令序列,其中mfence
几乎毫无用处,除非您明确希望在测试开始/结束之前(但不是在执行rdstc
之前!
如果您检测重新排序的测试assert(t2 > t1)
那么我相信您不会测试任何东西。
撇开可能会或可能不会阻止 CPU 及时看到第二个rdtsc
进行重新排序的return
和调用,CPU 不太可能(尽管可能!)重新排序两个rdtsc
即使一个紧接着另一个。
想象一下,我们有一个与rdtsc
完全相同的rdtsc2
,但写ecx:ebx
1。
执行
rdtsc
rdtsc2
很有可能ecx:ebx > edx:eax
,因为 CPU 没有理由在rdtsc
之前执行rdtsc2
。
重新排序并不意味着随机排序,它意味着如果当前指令无法执行,则寻找其他指令。
但是rdtsc
不依赖于任何先前的指令,因此在遇到 OoO 内核时不太可能延迟。
然而,奇特的内部微架构细节可能会使我的论文无效,因此我之前的陈述中可能有一个词。
1我们不需要这个更改的指令:寄存器重命名可以做到这一点,但如果你不熟悉它,这将有所帮助。
mfence用于在rdtsc之前强制在 CPU 中进行序列化。
通常你会在那里找到cpuid(这也是序列化指令)。
引用英特尔手册中有关使用rdtsc的内容将使其更清晰
从英特尔奔腾处理器开始,大多数英特尔 CPU 支持 代码的无序执行。目的是优化 由于不同的指令延迟而受到的处罚。不幸 此功能不保证 单个编译的 C 指令将遵循 指令本身编写在源 C 文件中。当我们打电话时 RDTSC 指令,我们假装该指令将是 在代码的开头和结尾精确执行 测量(即,我们不想测量执行的编译代码 在 RDTSC 呼叫之外或在呼叫之间执行 他们自己)。 解决方案是在之前调用序列化指令 称RDTSC为RDTSC之一。序列化指令是指令 强制 CPU 完成 C 的每个先前指令 代码,然后继续执行程序。通过这样做,我们保证 只有正在测量的代码才会在 在 RDTSC 调用之间,并且不会执行该代码的任何部分 通话外。
TL;DR 版本 - 如果在 rdtsc 之前没有序列化指令,您不知道该指令何时开始执行,从而使测量可能不正确。
提示 - 尽可能使用 rdtscp。
根据我的测试,找不到 cpu 重新排序。
仍然不能保证它可能发生 - 这就是为什么原始代码必须"memory"
指示可能的内存破坏者阻止编译器重新排序它的原因。