unsigned int lo = 0; unsigned int hi = 0; __asm__ __volatile__ ( "mfence;rdtsc" : "=a"(lo), "=d"(hi) : : "memory" );
在上面的代码中,是否有必要?
根据我的testing,没有findcpu reorder。
testing代码片段如下。
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
。
简单地说,在任何先前的指令在本地完成之前,确保没有新的指令开始。 看到我的这个答案对地方的更详细的解释 。
它也不会像mfence
那样耗尽存储缓冲区,并且不像cpuid
那样破坏寄存器。
因此, lfence / rdtsc / lfence
是比mfence / rdtsc
更好的指令序列,其中mfence
几乎是无用的,除非您明确地希望在测试开始/结束之前完成以前的存储(而不是在执行rdstc
之前)。
如果你的测试检测重新排序是assert(t2 > t1)
那么我相信你什么都不会测试。
退出return
和通话可能会或可能不会阻止CPU看到第二个rdtsc
重新排序,这是不可能的(尽管可能!),即使一个接一个地重新排列两个rdtsc
,也是不可能的。
想象一下,我们有一个完全像rdtsc
但写入ecx:ebx
1 。
执行
rdtsc rdtsc2
很有可能是ecx:ebx > edx:eax
因为CPU 没有理由在rdtsc2
之前执行rdtsc
。
重新排序并不意味着随机排序,这意味着如果当前的那个不能被执行 ,那么寻找其他的指令。
但是rdtsc
不依赖于任何先前的指令,所以当被OoO内核遇到时不太可能被延迟。
然而,特殊的内部微观建筑细节可能会使我的论文无效,因此我之前的发言中可能会出现这个词。
1我们不需要这个改变的指令:寄存器重命名会做,但如果你不熟悉它,这将有所帮助。
mfence在rdtsc之前是强制在CPU中进行序列化的。
通常你会发现cpuid (这也是序列化指令)。
从英特尔手册引用关于使用rdtsc将使它更清晰
从英特尔奔腾处理器开始,大多数英特尔CPU支持代码的无序执行。 目的是为了优化由于不同的指令延迟造成的惩罚。 不幸的是,这个特性并不能保证单个编译的C指令的时间顺序将遵循写在源C文件中的指令本身的顺序。 当我们调用RDTSC指令时,我们假设指令将在被测代码的开始和结束时执行(即,我们不想测量在RDTSC调用之外执行或在自称)。 解决的办法是在调用RDTSC之前调用序列化指令。 序列化指令是在继续执行程序之前强制CPU完成每个C代码的指令的指令。 通过这样做,我们保证只有在测量的代码将在RDTSC调用之间执行,并且该代码的任何部分都不会在调用之外执行。
TL; DR版本 – 在rdtsc之前没有序列化指令,你不知道该指令何时开始执行测量可能不正确。
提示 – 尽可能使用rdtscp。
根据我的测试,没有找到cpu reorder。
仍然不能保证它可能发生 – 这就是为什么原始代码有"memory"
来指示可能的内存clobber阻止编译器重新排序它。