2
unsigned int lo = 0;
unsigned int hi = 0;
__asm__ __volatile__ (
    "mfence;rdtsc" : "=a"(lo), "=d"(hi) : : "memory"
);

mfence在上面的代码中,有必要吗?

根据我的测试,没有找到 cpu reorder。

测试代码片段如下所示。

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);
4

2 回答 2

5

执行合理测量所需的rdtsc是序列化指令。

众所周知,很多人以前使用cpuid rdtsc.
rdtsc需要从上到下进行序列化(阅读:必须退役之前的所有指令,并且必须在测试代码开始之前退役)

不幸的是,第二个条件经常被忽略,因为cpuid对于这项任务来说这是一个非常糟糕的选择(它破坏了 的输出rdtsc)。
在寻找替代品时,人们认为名称中带有“围栏”的说明就可以了,但这也是不正确的。直接来自英特尔:

MFENCE 不序列化指令流。

一条几乎是序列化的指令,将在以前存储不需要完成的任何测量中执行lfence​​。

简而言之,lfence确保在任何先前指令在本地完成之前没有新指令开始。有关locality 的更详细说明,请参阅我的这个答案。
它也不会像这样做那样耗尽存储缓冲区,也不会像mfence这样做那样破坏寄存器cpuid

因此lfence / rdtsc / lfence,一个比mfence / rdtsc, where更好的指令序列mfence几乎没有用,除非您明确希望在测试开始/结束之前完成先前的存储(但不是在rdstc执行之前!)。


如果您检测重新排序的测试是,assert(t2 > t1)那么我相信您不会测试任何内容。
忽略return可能会或可能不会阻止 CPU 及时看到第二个rdtsc进行重新排序的调用,即使一个紧接着另一个,CPU 也不太可能(尽管可能!)重新排序两个rdtsc

想象一下,我们有一个完全一样的,但写的rdtsc21rdtscecx:ebx

执行

rdtsc
rdtsc2

很有可能是ecx:ebx > edx:eax因为 CPU之前没有理由执行。 重新排序并不意味着随机排序,它意味着如果当前的指令无法执行,则寻找其他指令。 但不依赖任何先前的指令,因此在遇到 OoO 核心时不太可能延迟。 然而,特殊的内部微架构细节可能会使我的论文无效,因此我之前的陈述中可能会出现这个词。rdtsc2rdtsc

rdtsc


1我们不需要这条修改后的指令:寄存器重命名就可以了,但如果您不熟悉它,这会有所帮助。

于 2017-01-22T14:56:28.753 回答
4

mfence可以在rdtsc之前强制在 CPU 中进行序列化。

通常你会在那里找到cpuid(这也是序列化指令)。

引用英特尔手册中有关使用rdtsc的内容会更清楚

从 Intel Pentium 处理器开始,大多数 Intel CPU 都支持代码的乱序执行。目的是优化由于不同指令延迟导致的惩罚。不幸的是,此功能不能保证单个编译的 C 指令的时间序列将尊重指令本身的序列,如源 C 文件中所写的那样。当我们调用 RDTSC 指令时,我们假设该指令将在被测量代码的开头和结尾准确执行(即,我们不想测量在 RDTSC 调用之外执行或在 RDTSC 调用之间执行的编译代码称自己)。解决方案是在调用 RDTSC 之前调用序列化指令。序列化指令是强制 CPU 在继续执行程序之前完成 C 代码前面的每条指令的指令。通过这样做,我们保证在 RDTSC 调用之间仅执行正在测量的代码,并且该代码的任何部分都不会在调用之外执行。

TL;DR 版本 - 在 rdtsc 之前没有序列化指令,您不知道该指令何时开始执行,从而使测量可能不正确。

提示 - 尽可能使用 rdtscp。

根据我的测试,没有找到 cpu reorder。

仍然不能保证它可能会发生 - 这就是为什么原始代码必须"memory"指出可能的内存破坏,以防止编译器重新排序它。

于 2017-01-22T04:13:15.657 回答