具体指示的延迟和评估 也许您可以在实际工作之间使用数据依赖关系? Re:您在Fence OoO执行壁垒之间进行CMC的第二次实验

问题描述

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

即使流水线的存在和执行端口上的争用的存在,我也想通过以可预测的方式执行一系列指令的方式执行某些指令来实现周期精确的延迟。例如,如果指令x占用2个周期,并且无法进行流水线处理,那么通过执行x 4次,我希望可以放置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指令时,似乎在i9900K机器中1 CDQ指令未映射为1cycle。但是,当我查看agner的优化表时,似乎CMC和CDQ指令确实没有什么不同。是不是因为CMC指令背靠背彼此之间没有依赖关系,而CDQ指令之间确实存在依赖关系?

此外,如果我们认为可变延迟不是由rdtsc引起的,也不是因为中断或其他争用问题引起的。那么看来CMC指令可用于延迟1个核心周期,对吗?因为我将内核固定为以3.6GHz时钟频率运行,所以该时钟频率假定为i9900k上的TSC时钟频率。.我确实看过所提到的问题,但无法捕获确切的细节。

解决方法

您有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?但这具有两次执行RDTSC的最低开销,如果您具有“ waitpkg” ISA扩展名,则至少需要RDTSC加上TPAUSE。 (您不在i9-9900k上)。如果要在整个过程中停止乱序执行,也需要lfence

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

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


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

如果没有考虑周围的代码并且不知道,那么实际上不可能有几个时钟周期的小延迟,它小于无序调度程序的大小 1 您要执行的确切的微体系结构。

脚注1:Skylake派生的uarches上的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执行人员的更多信息。

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


CMC是64位模式下可用的少数单字节指令之一,您可以重复该操作以创建 latency 瓶颈(在大多数CPU上,每条指令1个周期),而无需访问内存(像lodsb一样,在合并到RAX的低字节时遇到瓶颈。 xchg eax,reg也可以使用,但是在Intel上只有3微秒。

如果您从已知的CF状态开始并使用奇数或偶数个CMC指令,使CF = 0,那么您可以使用adc reg,0将dep链耦合到特定指令,而不是lfence。 。否则cmovc same,same会使寄存器值取决于CF而无需修改它,而不管CF是否被设置。

但是,如果连续太多而无法处理uop缓存,则单字节指令会产生奇怪的前端效果。如果您无限期地重复播放,这会使CDQ变慢。显然,Skylake在传统解码器中只能以1 /时钟对其进行解码。 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处于最大加速状态时更长。


Re:您在Fence OoO执行壁垒之间进行CMC的第二次实验

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

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

是不是因为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?)。对于https://uops.info/,Coffee Lake的重复次数较小时,数字更接近准确,显示为0.6 c的吞吐量。 (但是,如果您查看详细的细分,则展开计数为500 https://www.uops.info/html-tp/CFL/CDQ-Measurements.html可以确认Coffee Lake仍然存在前端瓶颈。

但是将重复计数增加到大约20个(如果对齐的话)将导致与Agner相同的传统解码瓶颈。但是,如果您不使用lfence,则解码可能会比执行提前很多,所以效果不好。

CDQ是一个糟糕的选择,因为前端效果怪异,并且/或者是后端吞吐量瓶颈而不是延迟。但是,一旦前端经过重复的CDQ,OoO高管仍然可以看到它。 1字节的NOP可能会创建一个前端瓶颈,根据您尝试分离的两件事,此瓶颈可能会更有用。


顺便说一句,如果您不完全了解依赖关系链及其对乱序执行的影响,以及可能有关您正在使用的确切CPU的许多其他cpu体系结构详细信息(例如,如果需要,则存储缓冲区)分离任何商店),您将很难做任何有意义的事情。

如果只需要两件事之间的数据依赖关系就可以完成您需要做的事情,那么这可能会减少您需要了解的内容,以便做出与您所描述的目标类似的事情。

否则,您可能基本上需要了解所有这些答案(以及Agner Fog的微体系结构指南),以弄清实际问题如何转化为可以使CPU实际完成的工作。或意识到它做不到,您还需要其他东西。 (就像一个非常快速的有序CPU,也许是ARM,您可以在其中通过延迟序列/循环来控制独立指令之间的时序。)

相关问答

错误1:Request method ‘DELETE‘ not supported 错误还原:...
错误1:启动docker镜像时报错:Error response from daemon:...
错误1:private field ‘xxx‘ is never assigned 按Alt...
报错如下,通过源不能下载,最后警告pip需升级版本 Requirem...