【问题标题】:delays and measurement of specific instructions特定指令的延迟和测量
【发布时间】:2021-01-08 15:49:42
【问题描述】:

由于现代处理器即使对于 ALU 也使用了繁重的流水线,因此可以在一个周期内执行多次独立算术运算,例如,可以在 4 个周期内执行四次加法运算,而不是一次加法的 4 * 延迟。

即使存在流水线,并且存在执行端口上的争用,我也想通过执行一些指令来实现周期精确的延迟,以使执行一系列指令的时间是可预测的。例如,如果指令 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 指令映射到 1 个周期延迟。除了第一次增加 10 个 CMC(不是增加 10 而是增加 6~8 个)外,大多数时候增加 10 个 CMC 指令会增加 (10 +-2) 个 TSC 循环。 但是,当我将 CMC 更改为我最初在问题中使用的 CDQ 指令时,似乎 1 个 CDQ 指令没有映射到 i9900K 机器中的 1 个周期。但是,当我查看 agner 的优化表时,似乎 CMC 和 CDQ 指令实际上并没有什么不同。是不是因为 CMC 指令背靠背之间没有依赖关系,但是 CDQ 指令之间确实存在依赖关系?

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

【问题讨论】:

  • 除了像非 mips PIC 微控制器和其他类似情况的芯片外,你不能在测试环境之外执行此操作。在模拟中,通过受控的获取或完全可重复的执行,您可以手动调整它,但在现实世界中,即使只有一块主板,但肯定是在 x86 世界中,不可能始终如一地做到这一点。不是花时间尝试的东西。您希望使用外围设备进行准确的计时,该外围设备旨在做您想做的事情。
  • 在软件中,你通常可以做到“至少这么长”,我可以让它不少于这个时间,但它可能会更长。这对于使用各种外围设备或其他东西很有用,但对于不超过和不低于时间的准确度来说不是。
  • 然后扔进一个操作系统,一切都变得更糟了。
  • 如果出于某种原因您正在寻找一个绝对依赖于自身的单字节指令(将执行限制为 1 个/时钟的延迟瓶颈),cmc(切换 CF)可能会成功。但这对于您延迟循环的总体目标几乎没有用处。可能与lfence 结合使用,但这可能会比您想要的延迟更长的时间,具体取决于任何现有的飞行指令需要多长时间。例如缓存未命中加载。
  • @JaehyukLee:对于“极小而准确”的延迟,唯一的选择是放弃,然后重新评估您错误地认为自己想要它的原因。对于内核代码;对于精度稍低的更长延迟,您可以考虑在“TSC 截止时间模式”中使用本地 APIC 计时器(可能对 IRQ 退出时间进行一些调整)和/或与性能监控计数器类似。

标签: assembly x86 delay microbenchmark timedelay


【解决方案1】:

您有 4 个主要选项:

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

这可能是一个 X-Y 问题,或者如果不深入了解您想要延迟分离的两件事的具体细节,至少是无法解决的。 (例如,在加载和存储地址之间创建数据依赖关系,并用一些指令延长该 dep 链)。对于非常短的延迟,在任意代码之间没有通用的答案。

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

对于“极小而准确”的延迟,唯一的选择是放弃,然后重新评估您错误地认为自己想要它的原因。

对于内核代码;对于精度稍低的较长延迟,您可以考虑在“TSC 截止时间模式”中使用本地 APIC 计时器(可能对 IRQ 退出时间进行一些调整)和/或与性能监控计数器类似。

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

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

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


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

几个时钟周期的小延迟,小于乱序调度器大小1,如果不考虑周围的代码并知道您正在执行的确切微架构。

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

如果您可以在要分离的两个事物之间创建数据依赖关系,那么您也许可以在它们的执行之间创建一个最小延迟。 有一些方法可以耦合依赖关系链接到另一个寄存器而不影响其值,例如and eax, 0 / or ecx, eax 使 ECX 依赖于写 EAX 的指令而不影响 ECX 的值。 (Make a register depend on another one without changing its value)。

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

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


CMC 是少数 64 位模式下可用的单字节指令之一,您可以重复这些指令以创建 延迟 瓶颈(在大多数 CPU 上每条指令 1 个周期),而无需访问内存 (就像lodsb 合并到 RAX 的低字节时遇到的瓶颈)。 xchg eax, reg 也可以,但在英特尔上是 3 微秒。

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

但是,当 uop 缓存无法处理的行中的指令过多时,单字节指令会产生奇怪的前端效果。如果您无限期地重复它,这就是减慢 CDQ 的原因。显然 Skylake 只能在传统解码器中以 1/clock 解码它。 Can the simple decoders in recent Intel microarchitectures handle all 1-µop instructions?。这可能没问题和/或你想要的。每 3 字节指令 3 个周期将使该代码由 uop 缓存缓存,例如 imul eax, eaximul eax, 0。但也许最好避免使用应该运行缓慢的代码污染 uop 缓存。

在 LFENCE 指令之间,cld 是 3 uops,并且在 Skylake 上具有 4c 吞吐量,因此,如果您在延迟开始/结束时使用 lfence,则可以使用。


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

或者,如果 CPU 恰好以空闲速度(通常为 800MHz)运行,则延迟以纳秒为单位将比 CPU 处于最大 turbo 时长得多。


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

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

第一个 lfence 必须等待之前执行的任何指令才能从无序后端退出(ROB = 重新排序缓冲区,Skylake 系列上的 224 个融合域微指令)。如果其中包括任何可能在缓存中丢失的负载,则您的等待时间可能会有很大差异,并且比您可能想要的要长得多。

是不是因为 CMC 指令背靠背之间没有依赖关系,而 CDQ 指令之间却有依赖关系?

你倒过来了CMC 对前一个 CMC 具有真正的依赖关系,因为它读取和写入进位标志。就像not eax 对之前的 EAX 值有真正的依赖。

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

Agner Fog 的数据基于对大量重复指令的测试,这显然是 1/时钟的传统解码吞吐量的瓶颈。 (再次,请参阅Can the simple decoders in recent Intel microarchitectures handle all 1-µop instructions?)。对于 Coffee Lake 的小重复计数,https://uops.info/ 数字更接近准确,显示为 0.6 c 吞吐量。 (但是,如果您查看详细的细分,https://www.uops.info/html-tp/CFL/CDQ-Measurements.html 的展开计数为 500 可以确认 Coffee Lake 仍然存在前端瓶颈)。

但是将重复计数增加到大约 20 次以上(如果对齐)将导致 Agner 看到的相同的遗留解码瓶颈。但是,如果您不使用 lfence,则 decode 可能会远远领先于执行,所以这并不好。

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


顺便说一句,如果您不完全了解依赖链及其对乱序执行的影响,并且可能还有一堆关于您正在使用的确切 CPU 的其他 cpu 架构详细信息(例如,如果您愿意,可以存储缓冲区将任何商店分开),您将很难尝试做任何有意义的事情。

如果您可以仅通过两件事之间的数据依赖关系来做您需要的事情,那可能会减少您需要了解的内容数量,以实现您所描述的目标。

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

【讨论】:

    猜你喜欢
    • 2019-08-27
    • 2020-02-22
    • 1970-01-01
    • 2015-10-19
    • 1970-01-01
    • 2021-01-02
    • 1970-01-01
    • 2016-11-16
    • 1970-01-01
    相关资源
    最近更新 更多