特定指令的延迟和测量

delays and measurement of specific instructions

因为现代处理器甚至对 ALU 都使用重型流水线,所以独立算术运算的多次执行可以在一个周期内执行,例如,四个加法运算可以在 4 个周期内执行,而不是 4 * 一个加法的延迟。

即使存在管道,并且存在执行端口争用,我也想通过执行一些指令来实现周期精确的延迟,执行指令序列的时间是预测的table。例如,如果指令 x 需要 2 个周期,并且不能流水线化,那么通过执行 x 四次,我希望我可以延迟 8 个周期。

我知道这对于用户空间来说通常是不可能的,因为内核可以在执行序列之间进行干预,并可能导致比预期更多的延迟。但是,我假设此代码在内核端执行,没有中断或没有噪音的隔离内核。

看了https://agner.org/optimize/instruction_tables.pdf后,我发现CDQ指令不需要内存操作,它的延迟和倒数吞吐量都需要1个周期。如果我理解正确的话,这意味着如果CDQ使用的端口没有争用,它可以在每个周期执行这条指令。为了对其进行测试,我将 CDQ 置于 RDTSC 定时器之间,并将核心频率设置为标称核心频率(希望它与 TSC 周期相同)。此外,我将两个进程固定到超线程核心;一个落入 while(1) 循环,另一个执行 CDQ 指令。好像增加一条指令会增加1-2个TSC周期。

但是,我担心需要大量 CDQ 指令来放置大延迟的情况,例如 10000,这可能至少需要 5000 条指令。如果代码太大而无法放入指令缓存并导致缓存未命中和 TLB 未命中,则可能会在我的延迟中引入一些抖动。我试过使用简单的 for 循环来执行 CDQ 指令,但不能确定使用 for 循环(用 jnz、cmp 和 sub 实现)是否可以,因为它也可能在我的延迟中引入一些意想不到的噪音。谁能确认我是否可以这样使用CDQ指令?

添加问题

用多条CMC指令测试后,似乎10条CMC指令增加了10个TSC周期。我使用下面的代码来测量执行 0、10、20、30、40、50

的时间
    asm volatile(                                                                                                                                                                                                                                                                               
        "lfence\t\n"                                                                                                                                                                                                                                                                            
        "rdtsc\t\n"                                                                                                                                                                                                                                                                             
        "lfence\t\n"                                                                                                                                                                                                                                                                            
        "mov %%eax, %%esi\t\n"
                                                                                                                                                                                                                                                                                                
        "cmc\n\t" // CMC * 10, 20, 30, 40, ...
                                                                                                                                                                                                                                                                                                
        "rdtscp\n\t"                                                                                                                                                                                                                                                                            
        "lfence\t\n"                                                                                                                                                                                                                                                                            
        "sub %%esi, %%eax\t\n"
        :"=a"(*res)
        :
        : "ecx","edx","esi", "r11"
    );

    printf("elapsed time:%d\n", *res);

我得到了 44-46、50-52、62-64、70-72、80-82、90-92(无 CMC、10CMC、20CMC、30CMC、40CMC、50CMC)。当 RDTSC 结果在每次执行时变化 0~2 TSC 周期时,似乎 1CMC 指令映射到 1cycle 延迟。除了第一次加10个CMC(不是加10而是6~8),大多数时候加10个CMC指令都会加(10 +-2)个TSC cylces。 但是,当我将 CMC 更改为最初在问题中使用的 CDQ 指令时,似乎 1 CDQ 指令未映射到 i9900K 机器中的 1cycle。但是,当我查看 agner 的优化 table 时,CMC 和 CDQ 指令似乎并没有什么不同。是不是因为 CMC 指令背靠背相互之间没有依赖关系,但 CDQ 指令之间确实存在依赖关系?

此外,如果我们考虑变量延迟是由 rdtsc 引起的,而不是因为中断或其他争用问题..那么 CMC 指令似乎可以用于延迟 1 个核心周期,对吧?因为我将我的内核固定在 3.6GHz 时钟频率的 运行 上,假设它是 i9900k 上的 TSC 时钟频率。我确实查看了引用的问题,但无法了解确切的细节。

您有 4 个主要选项:

  • 通过为第一个操作(的结果)提供数据依赖性来延迟第二个操作。
  • lfence,固定延迟序列,lfence。这两者都只能给出最小的延迟;可能会更长,具体取决于 CPU 频率缩放 and/or 中断。
  • 在 rdtsc 上旋转直到截止日期(您以某种方式计算,例如基于较早的 rdtsc),或者根据 TSC 截止日期进行更长的睡眠,例如使用本地 APIC。
  • 放弃并使用不同的设计,或使用 in-order 微控制器,您可以在固定时钟频率下获得可靠的 cycle-accurate 时序。

这可能是一个 X-Y 问题,或者至少在不深入了解您想要延迟分离的两件事的具体细节的情况下是无法解决的。 (例如,在加载和 store-address 之间创建数据依赖关系,并使用一些指令延长该 dep 链)。没有 general-case 答案可以在任意代码之间工作以实现非常短的延迟。

如果你只需要几个时钟周期的精确延迟,那你就完蛋了;超标量 out-of-order 执行、中断和可变时钟频率使得这在一般情况下基本上是不可能的。正如@Brendan 解释的那样:

For "extremely small and accurate" delays the only option is to give up then reassess the reason why you made the mistake of thinking you wanted it.

For kernel code; for longer delays with slightly less accuracy you could look into using local APIC timer in "TSC deadline mode" (possibly with some adjustment for IRQ exit timing) and/or similar with performance monitoring counters.

对于几十个时钟周期的延迟,spin-wait RDTSC 具有您正在寻找的值。 How to calculate time for an asm delay loop on x86 linux? 但是如果您有“waitpkg”ISA 扩展,则执行 RDTSC 两次或 RDTSC 加 TPAUSE 的开销最小。 (你不在 i9-9900k 上)。如果你想在整个过程中停止 out-of-order exec,你还需要 lfence

如果您需要“每 20 ns”或其他时间做某事,则增加截止日期,而不是尝试在其他工作之间进行固定延迟。所以其他工作的变化不会累积错误。但是一个中断会让你远远落后并导致 运行 完成你的其他工作 back-to-back 直到你赶上来。因此,除了检查截止日期外,您还需要检查是否远远落后于截止日期并获取新的 TSC 样本。

(TSC 在现代 x86 上以恒定频率滴答,但核心时钟不是:有关详细信息,请参阅 How to get the CPU cycle count in x86_64 from C++?


也许您可以在实际工作之间使用数据依赖性?

几个时钟周期的小延迟,小于 out-of-order 调度程序大小 1,如果不 将周围的代码纳入其中,是不可能实现的考虑 并了解您正在执行的确切微体系结构。

脚注 1:Skylake-derived uarches 上有 97 个条目 RS,尽管有一些证据表明它不是真正的统一调度程序:某些条目只能容纳某些类型的 uops。

如果您可以在您试图分离的两个事物之间创建数据依赖关系,您可能能够以这种方式在它们的执行之间创建一个最小延迟。有将依赖链耦合到另一个寄存器而不影响其值的方法,例如and eax, 0 / or ecx, eax 使 ECX 依赖于写入 EAX 的指令而不影响 ECX 的值。 ().

例如在两次加载之间,您可以创建一个数据依赖关系,从一个加载结果到后来加载的加载地址,或存储地址。将两个商店地址与依赖链耦合在一起不太好;在知道地址后,第一个存储可能会花费大量额外时间(例如,对于 dTLB 未命中),所以两个存储最终最终提交 back-to-back。如果您想在第二家商店之前延迟,您可能需要在两家商店之间 mfence 然后 lfence。另请参阅 Are loads and stores the only instructions that gets reordered? 了解更多关于 OoO exec across lfence(以及 Skylake 上的 mfence)的信息。

这可能也需要在 asm 中编写您的“实际工作”,除非您能想出一种方法来使用一个小的内联 asm 语句从编译器中“清洗”数据依赖性。


CMC 是 64 位模式中为数不多的 single-byte 指令之一,您可以重复这些指令以创建 延迟 瓶颈(大多数情况下每条指令 1 个周期CPUs) 而无需访问内存(如 lodsb 合并到 RAX 低字节时的瓶颈)。 xchg eax, reg 也可以,但在 Intel 上是 3 微指令。

如果您从已知的 CF 状态开始并使用奇数或偶数的 CMC 指令,那么您可以使用 adc reg, 0 将该 dep 链耦合到特定指令中,而不是 lfence,这样 CF=0那一点。或者 cmovc same,same 将使寄存器值依赖于 CF 而无需修改它,无论 CF 是否已设置或清除。

然而,single-byte 指令会产生奇怪的 front-end 效果,当你有太多连续的指令让 uop 缓存无法处理时。如果你无限期地重复它,这就是减慢 CDQ 的原因;显然,Skylake 只能在传统解码器中以 1/clock 对其进行解码。 。那可能就可以了 and/or 你想要什么。每 3 字节指令 3 个周期将使该代码被 uop 缓存缓存,.g imul eax, eaximul eax, 0。但也许最好避免使用应该 运行 缓慢的代码污染 uop 缓存。

在 LFENCE 指令之间,cld 是 3 微指令,并且在 Skylake 上具有 4c 的吞吐量,因此如果您在 start/end 的延迟处使用 lfence,则可以使用。


当然,任何 dead-reckoning 某些指令(不是 rdtsc)的延迟将取决于核心时钟频率 , 不是参考频率。充其量只是 最小 延迟;如果在延迟循环期间出现中断,则总延迟将接近中断处理时间的总和加上 delay-loop 花费的时间。

或者如果 CPU 恰好 运行 处于怠速(通常为 800MHz),则以纳秒为单位的延迟将比 CPU 处于最大涡轮时长得多.


回复:您在 lfence OoO exec 障碍之间使用 CMC 进行的第二次实验

是的,您可以非常准确地控制两个 lfence 指令之间或 lfence 和 rdtscp 之间的核心时钟周期,使用简单的依赖链、pause 指令或某些执行单元上的吞吐量瓶颈), 可能是整数或 FP 分频器。 但我假设您的实际用例关心第一个 lfence 之前的内容和第二个 lfence 之后的内容之间的 延迟。

第一个 lfence 必须等待之前执行的任何指令从 out-of-order back-end 退出(ROB = 重新排序缓冲区,224 fused-domain 微指令Skylake-family)。如果这些包括任何可能在缓存中丢失的加载,您的等待时间可能会有很大差异,并且比您可能想要的要长得多。

Is it because CMC instructions back to back have no dependency on each other but CDQ instructions do have a dependency in between them?

你有倒退CMC 对以前的 CMC 有真正的依赖性,因为它读取和写入进位标志。就像 not eax 对之前的 EAX 值有真正的依赖。

CDQ 没有:它读取 EAX 并写入 EDX。寄存器重命名使得在同一个时钟周期内多次写入 RDX 成为可能。例如Zen 每个时钟可以 运行 4 cdq 条指令。您的 Coffee Lake 每个时钟可以 运行 2 个 CDQ(0.5c 吞吐量),在 back-end 端口上存在瓶颈,它可以 运行 在(p0 和 p6)上。

Agner Fog 的数字是基于对大量重复指令的测试,显然是 legacy-decode 1/时钟吞吐量的瓶颈。 (再次参见 )。 https://uops.info/ 对于 Coffee Lake 的小重复计数,数字更接近准确,显示为 0.6 c 吞吐量。 (但是,如果您查看详细的细分,展开计数为 500 https://www.uops.info/html-tp/CFL/CDQ-Measurements.html 可以确认 Coffee Lake 仍然存在 front-end 瓶颈)。

但是将重复计数增加到超过 20 次(如果对齐)将导致与 Agner 看到的相同的 legacy-decode 瓶颈。但是,如果您不使用 lfence,则解码可能会远远领先于执行,因此这并不好。

CDQ 是一个糟糕的选择 因为奇怪的 front-end 效果,and/or 是 back-end 吞吐量瓶颈而不是延迟。但是一旦 front-end 通过重复的 CDQ,OoO exec 仍然可以看到它。 1 字节 NOP 可能会造成 front-end 瓶颈,这可能更有用,具体取决于您尝试分离的两件事。


顺便说一句,如果您不完全理解依赖链及其对 out-of-order 执行的影响,并且可能还有一堆其他 cpu-architecture 确切 CPU 的详细信息,您使用(例如,如果你想分离任何存储,存储缓冲区),你将很难尝试做任何有意义的事情。

如果您只需要两个事物之间的数据依赖性就可以做您需要的事情,那么这可能会减少您需要了解的东西的数量,以实现您所描述的目标。

否则,您可能需要基本上理解所有这个答案(以及 Agner Fog 的微体系结构指南),才能弄清楚您的实际问题如何转化为您可以实际让 CPU 做的事情。或者意识到它不能,你需要别的东西。 (可能是非常快的 in-order CPU,也许是 ARM,你可以在某种程度上控制具有延迟序列/循环的独立指令之间的时序。)