多个nop指令不一定比单个nop指令花费更长的时间

4
我正在使用C++中的rdtsc计时多个NOP指令和一个单独的NOP指令。然而,我执行NOP指令的数量增加时,并没有在相应的周期数上看到增加。我不知道这是为什么。我的CPU型号是Intel Core i7-5600U @ 2.60Ghz。
以下是代码:
#include <stdio.h>

int main() {
    unsigned long long t;

    t = __rdtsc();
    asm volatile("nop");
    t = __rdtsc() - t;
    printf("rdtsc for one NOP: %llu\n", t);

    t = __rdtsc();
    asm volatile("nop; nop; nop; nop; nop; nop; nop;");
    t = __rdtsc() - t;
    printf("rdtsc for seven NOPs: %llu\n", t);

}

我得到的数值为:

rdtsc for one NOP: 78
rdtsc for seven NOPs: 91

rdtsc for one NOP: 78
rdtsc for seven NOPs: 78

在未设置处理器亲和力的情况下运行时, 当像 $ taskset -c 0 ./nop$ 这样设置处理器亲和力时,结果为:
rdtsc for one NOP: 78
rdtsc for seven NOPs: 78

rdtsc for one NOP: 130
rdtsc for seven NOPs: 169

rdtsc for one NOP: 78
rdtsc for seven NOPs: 143

为什么会这样呢?

由于你使用了x86架构,以及你编写基准测试的方式,这并不令人意外。你实际上想要做什么? - old_timer
我正在尝试在十分之一微秒的范围内进行延迟。但是当我使用nanosleep函数,即使将睡眠间隔设置为仅一个纳秒,执行nanosleep函数所需时间也会超过20000个周期(根据rdtsc)。这就是为什么我要试图直接使用nops来引发非常微小的延迟。 - fraiser
2
你可能需要使用 pause 指令。在 Skylake 及之后的处理器上,它会空闲约100个周期;在早期的 Intel 处理器上,它会空闲约5个周期。或者可以使用 RDTSC 自旋。在现代超标量/乱序 x86 处理器上,插入 NOPs 指令永远不会可靠地工作,因为这些处理器具有巨大的重排序缓冲区!你的“休眠”时间甚至可能不足以清空乱序执行缓冲区(ROB)。指令没有一个你可以简单相加的成本。如何预测现代超标量处理器上操作的延迟,并如何手动计算? - Peter Cordes
1
@PeterCordes 在 RDTSC 上自旋可以工作! - fraiser
你不能像那样让延迟来控制时间代码,需要使用定时器。 - old_timer
1个回答

5
您在这里得出的结果可能是测量噪声和/或频率缩放,因为您在printf从系统调用返回后立即启动第二个间隔的计时器。
RDTSC计数参考周期而不是核心时钟周期,因此您基本上只是发现CPU频率。(较低的核心时钟速度=相同数量的核心时钟运行两个rdtsc指令需要更多的参考周期)。您的RDTSC指令基本上是连续的;与rdtsc本身解码的uop数量相比,nop指令可以忽略不计(在包括您的Broadwell在内的正常CPU上)。
此外,RDTSC可以通过乱序执行重新排序。虽然nop没有任何CPU必须等待的操作;它只是延迟了前端0.25或1.75个周期以发出第二个rdtsc的uops。(实际上,我不确定微代码序列器是否可以在一个指令的uop与另一个指令的uop在同一个周期发送。所以也许是1或2个周期)。
我的回答在如何从C++中获取x86_64中的CPU周期计数?上有关于RDTSC如何工作的背景。
您可能需要pause指令。在Skylake及更高版本中,它会闲置大约100个核心时钟周期,或者在早期的Intel核心上闲置大约5个周期。或者在PAUSE + RDTSC上旋转。如何在x86 Linux上计算asm延迟循环的时间?显示了一个可能有用的延迟自旋循环,它会睡眠一定数量的RDTSC计数。您需要知道参考时钟速度以将其与纳秒相关联,但通常在Intel CPU上为额定的最大非Turbo时钟,例如4.0 GHz Skylake的4008 MHz。
如果可用,tpause将TSC时间戳作为唤醒时间。(请参阅链接)。但目前只适用于低功耗Tremont。
在现代超标量/乱序x86和巨大的重排序缓冲区上插入NOP永远不会可靠地工作!现代x86不是可以计算嵌套延迟循环的微控制器。如果周围的代码没有瓶颈在前端,OoO执行只会隐藏通过管线传输NOP的成本。 指令本身没有成本,你只需要累加。 要模拟指令的成本,您需要知道其延迟、前端uop计数以及它需要哪些后端执行端口。 还要考虑到对流水线的任何特殊影响,比如lfence在后续指令发出之前等待所有先前的uop退休。请参阅每条汇编指令需要多少CPU周期?
同时还需参阅预测现代超标量处理器操作的延迟需要考虑哪些因素,并如何手动计算?
需要注意的是,您期望的约100纳秒的“睡眠”时间甚至不足以清空乱序执行缓冲区(ROB),如果有正在进行中的缓存未命中,或者可能是非常慢的ALU依赖项链(除了人为情况外,这种情况不太可能出现)。 因此,您可能不希望执行类似于lfence的任何操作。

1
@Peter - 这被记录为单向屏障,但我没有发现任何证据表明它的行为不同于完全屏障(当我尝试时,它似乎具有类似lfence的完全屏障语义)。我猜它仍然有用,因为它与指令绑定,所以您不需要两个屏障:之前和之后(以防止可能成为问题的两种不同类型的移动)。 - BeeOnRope
@PeterCordes “同时 RDTSC 也可以被乱序执行重新排序” 这里可能是多了一个“and”,或者缺少一个词。 - Sep Roland
@PeterCordes 嗯,我也建议你可以可能用“rdtscp”_只替换_“lfence; rdtsc; lfence”,因为据我所知,在当前硬件上,它具有完整的lfence效果,尽管文档上有所说明。话虽如此,考虑到硬件,也许这是错误的:从周围代码的角度来看,它具有完整的lfence效果,但想必不是组成rdtscp本身的所有uop:假设逻辑上的“内部”lfence在开始附近,我猜其他操作可能会潜入rdtscp uops当中。 - BeeOnRope
1
@BeeOnRope:嗯,是的,正如我们所讨论的那样,不清楚为什么 lfence; rdtsc; lfence 会更好,但在实践中似乎确实如此。因此,我认为在真实的硬件上,假设 lfence uops(几乎)是第一位,rdtscp = lfence; rdtsc 几乎完全相同。无论你是否在定时区域底部跟随另一个 lfence 是完全不同的问题。 - Peter Cordes
1
但是,是的,最老的准备好的任务优先级=没有问题,这也是我的想法,因为执行单元完全流水线化,我不希望rdtsc接触除法器。物理寄存器分配发生在发出/重命名/分配时间,但有一些取决于执行。 (顺便说一句,我不知道所有22个rdtscp uop实际上是做什么的,以及在执行所有较早uop之前有多少可以在lfence之前进行时钟采样。即使不一定需要,似乎将lfence部分放在第一位是一个明智的选择,尽管)。 - Peter Cordes
显示剩余8条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接