什么是部分标志延迟?

14
我刚看了Peter Cordes的这篇回答,他说:

如果发生了,部分标志暂停会在读取标志时发生。P4从不发生部分标志暂停,因为它们永远不需要合并。它有假依赖关系。几个答案/评论混淆了术语。他们描述了一个假依赖性,但随后称其为部分标志暂停。这是一种放慢速度的情况,因为仅写入了一些标志,但“部分标志暂停”是在预-SnB英特尔硬件上发生的情况,当必须合并部分标志写入时。英特尔SnB系列CPU插入了一个额外的uop来合并标志而无需停顿。Nehalem和更早期的处理器会停滞大约7个周期。我不确定在AMD CPU上的代价。

我觉得我还不理解什么是“部分标志暂停”?怎样才能知道它已经发生?除了读取标志时 有时候 会触发此事件,还有什么其他的触发条件吗?什么是合并标志?在哪种情况下“一些标志被写入”,但不会发生部分标志合并?了解标志暂停需要了解哪些内容?


Peter Cordes等人可能有更全面的解释,但我理解的方式是,在寄存器重命名中标志位是单独重命名的。对于设置所有标志位的指令(其中大部分),所有这些“寄存器”的状态可以一次性重置,但对于只影响子集标志位的指令,实际标志值需要从当前指令以及上一个设置剩余标志位的指令中合并,如果这样说有意义的话。这种合并(有时)需要额外的时间。 - 500 - Internal Server Error
我的心理模型是指令在串行中操作全局标志寄存器?这不是真的吗?如果Peter参与讨论,期待他的回答。 - Evan Carroll
2
@EvanCarroll:EFLAGS当然被重命名了。如果你没有打破WAW风险,add怎么可能每个时钟周期有4个吞吐量呢?(是的,不同组的标志分别重命名,所以inc也可以每个时钟周期有4个吞吐量,并且没有对FLAGS的输入依赖性,就像一些英特尔CPU可以将ahal分别重命名写入一样。)正在努力回答,但请参阅Agner Fog的微架构指南:http://agner.org/optimize/。他解释了部分标志停顿和合并。 - Peter Cordes
我会闭嘴等待答案。我不会撒谎,我曾经搜索过你的名字在亚马逊上。如果你出版有关x86、Linux或Radare的书籍,请收下我的钱。 - Evan Carroll
2个回答

12

一般而言,当一个消耗标志位的指令读取了一个或多个不是最近设置标志位的指令所写入的标志位时,会发生部分标志位停顿。

所以像inc这样仅设置一些标志位的指令(它不设置CF),并不会本质上导致部分停顿,但如果后续指令读取了inc未设置的标志位(CF)(而没有任何中间指令设置CF标志位),会导致停顿。这也意味着写入所有有趣标志位的指令永远不会涉及到部分停顿,因为当它们在标志位读取指令被执行的那一点是最新的标志设置指令时,它们必须已经写入了被使用的标志位。

因此,通常用于静态确定是否会出现部分标志位停顿的算法是,查看每个使用标志位的指令(通常包括jcc系列和cmovcc以及一些特殊指令,如adc),然后向后查找第一个设置任何标志位的指令,并检查它是否设置了消耗指令读取的所有标志位。如果没有,将发生部分标志位停顿。

从Sandy Bridge开始的后续架构并没有遭受部分标志位停顿,但某些情况下仍会因指令而增加一个额外的uop。规则略有不同,并且与上述停顿相比适用范围更窄。特别是,只有当消耗标志位的指令从多个标志位中读取,并且这些标志位是由不同的指令最后设置的时,才会添加所谓的标志合并uop。这意味着,例如,仅检查单个标志位的指令永远不会导致发出合并的uop。

从Skylake(和可能从Broadwell)开始,我找不到任何合并uops的证据。相反,uop格式已扩展为最多接受3个输入,这意味着重命名的进位标志和重新命名到一起的SPAZO组标志都可以用作大多数指令的输入。但像cmovbe这样具有两个寄存器输入的指令以及其条件be需要同时使用 C标志位和一个或多个SPAZO标志 。但是,大多数条件移动仅使用C和SPAZO标志之一,并且只占用一个uop。

示例

以下是一些示例。我们讨论"[partial flag] stalls"和"merge uops",但如上所述,任何给定体系结构最多只有其中之一适用,因此像"以下会导致停顿和合并的uop被发出"这样的语句应该读作"以下将在那些有部分标志位停顿的较旧体系结构上引起停顿,在那些使用合并uop而不是停顿的新型体系结构上引起合并uop"。

停顿和合并uop

add rbx, 5   ; sets CF, ZF, others
inc rax      ; sets ZF, but not CF
ja  label    ; reads CF and ZF

ja指令读取由addinc指令最后设置的CFZF标志位,因此插入了合并uop以将分别设置的标志位统一为一个,以供ja使用。在停顿体系结构上,由于ja从最近的标志设置指令未设置的CF中读取,因此会发生停顿。

仅停顿

add rbx, 5   ; sets CF, ZF, others
inc rax      ; sets ZF, but not CF
jc  label    ; reads CF

这会导致停顿,因为与先前的例子一样,读取了未被最后一个标志设置指令(这里是inc)设置的CF。在这种情况下,可以通过交换inc和add的顺序来避免停顿,因为它们是独立的,然后jc只会从最近的标志设置操作中读取。不需要合并uop,因为读取的标志(仅CF)都来自同一个add指令。

注意:这个案例正在讨论中(请参见评论)-但我无法测试它,因为在我的Skylake上找不到任何合并操作的证据。

没有停顿或合并uop

add rbx, 5   ; sets CF, ZF, others
inc rax      ; sets ZF, but not CF
jnz  label   ; reads ZF

这里不需要任何的停顿或合并单元,即使最后一条指令(inc)只设置了一些标志位,因为接下来的jnz只会读取 (inc) 设置的(一部分)标志位而不会读取其他标志位。因此,这种通用的循环惯用语(通常使用dec而不是inc)并没有根本性的问题。

这里有另一个示例,也不会造成任何停顿或合并 uop:

inc rax      ; sets ZF, but not CF
add rbx, 5   ; sets CF, ZF, others
ja  label    ; reads CF and ZF

在这里,ja 读取了 CFZF,并且存在一个不会设置 ZFinc(即部分标志位写入指令),但没有问题,因为 addinc 之后,并且写入了所有相关的标志位。

移位操作

sarshrshl 指令,在它们的变量和固定计数形式中的行为与上述描述不同(通常更差),并且在不同的体系结构中差异较大。这可能是由于它们的奇怪和不一致的标志处理所致1。例如,在许多体系结构中,在使用除 1 以外的计数值进行移位指令后读取任何标志位时,会出现类似于部分标志位暂停的情况。即使在最新的体系结构上,变量移位也会由于标志处理而有显着的 3 uops 成本(但不再有“暂停”)。

我不会在这里列出所有详细信息,但如果您想要所有详细信息,我建议在 Agner 的微体系结构文档中查找单词“shift”。

有些旋转指令在某些情况下也具有与移位类似的标志相关行为。


1 例如,根据移位计数是否为 0、1 或其他值设置不同的标志子集。


1
@PeterCordes - 奇怪,我写了一些测试(https://gist.github.com/travisdowns/42a3879bb2b03e7303128b0bb58e244b),但我在任何情况下都看不到额外的合并操作的证据。我希望第一种情况会有一个合并uop,第二种情况正在讨论,我希望第三种情况永远不会有一个合并uop,但对于我检查的所有性能计数器,每个“inc”,“add”,“jcc”三元组总共都有3个uops,并且所有变体的性能都相同。我认为这些uops会显示在perf计数器中?Skylake。 - BeeOnRope
1
@PeterCordes - 请看这个线程:看起来合并uops的实际发生可能比以前认为的要少得多,至少在Skylake上是如此,但在早期架构上也可能如此(我只是没有它们进行测试)。请查看此线程 - 看起来发生的情况实际上是由于缺乏宏融合而导致额外的uop,因此在许多情况下,没有额外的合并uop(但仍存在额外的uop)。我还没有深入调查过,但“inc”从不会产生合并uop是完全可能的。 - BeeOnRope
1
我的天啊,我从没注意到cmovbecmova在SKL上是2-uop指令。虽然从第一个操作数到目的地的延迟仍然为1个周期。一个cmp ebx, 123 / times 6 cmovbe ecx, ebx循环体(只通过ECX执行循环依赖),每6.5个周期运行一个迭代,而cmovbcmovz则为6.00。我认为你对指令有两个标志的输入分开的结论是合理的,例如jbe - Peter Cordes
1
@PeterCordes - 奇怪的是,setbe和其他指令也需要2个微操作。奇怪的是,它们只有一个输入,所以如果通用寄存器和标志寄存器可以互换,那么这似乎可以变成1个微操作。 - BeeOnRope
我想我的意思是它在前端的某个地方,因为那里生成了uop流?也就是说,我期望任何动态插入有序uop流中的_uop_都会在前端适当定义的位置发生,因为那是生成流的地方。它可能在ROB处,或者更确切地说是“在重命名时”,这是我认为前端和后端之间的分界线。我实际上不知道它插入的确切位置,很抱歉。 - BeeOnRope
显示剩余13条评论

1

修改uop的标志只能更新部分标志寄存器。RAT对于标志/eflags/rflags寄存器只有一个条目和一个掩码,显示由导致物理寄存器指向的条目被分配的uop更改的标志。如果发生一系列读写相同标志的指令,那么将为每个写操作分配一个单独的物理寄存器,并且每个读取使用先前的物理寄存器。在这些寄存器中将写入该标志,所有其他标志都将被清除。这就是为什么当从不在标志RAT条目掩码中的不同标志进行读取时,当前物理寄存器不能使用,因为它会读取一个清除的位而不是留下的实际标志状态。在旧的微架构中,会发生停顿,直到RRF中的标志寄存器状态有效(通过等待每个设置标志uop退休之前插入它们设置的位在RRF标志寄存器中,其中每个uop都被检查以了解它使用的体系结构寄存器/标志更改,这比x86宏操作更容易解释)。

在使用PRF方案(从SnB开始)的微架构中,当没有专用的RRF寄存器时,需要合并uop以保持统一的标志寄存器,否则退役RAT将指向一个无意义的物理寄存器,其中只有1个标志。每次部分标志修改指令(如incdec)后都会发生合并uop。由于add修改了所有6个状态标志,因此不需要合并uop。我认为这可能意味着在PRF方案中,状态、控制和系统标志是单独重命名的,因为add不需要合并uop。显然,CF标志与SPAZO集群重命名不同
部分寄存器暂停类似。RAT有2个条目来表示rax: 一个条目用于al/ax/eax/rax(通过条目中的大小指示符进行区分),另一个用于ah(两者在写入ax, eaxrax时都会更新,指向相同的寄存器)。它只需要2个来表示,因为只有2个互斥的寄存器。如果在较小的寄存器之一被写入并退役之前从eax读取,则分配器将暂停(因为ROB条目不能有2个相同操作数的依赖项),直到完整的寄存器出现在RRF中,然后它将重命名rax的RRF寄存器的两个条目。
在使用PRF方案的后期微架构中,这现在变得困难,因为不再保留单个rax的RRF。因此,需要使用合并uop,这也比以前微架构的停顿方法更快。
合并uop实现
  1. 合并uop的一个实现方式是在每次写入到部分标志/寄存器之前插入它,合并uop在写入新物理寄存器之前从完整寄存器/标志寄存器读取所有内容。然后将写入分配给相同的寄存器,这样写入就自然地合并了。接下来的读取可以读取寄存器/任何标志的任何部分。这基本上在每个部分标志写入指令和先前标志写入指令(部分或完整)之间设置了依赖链,并在每个部分寄存器写入和先前寄存器(完整/部分)写入之间设置了依赖链。在这种情况下,RAT从不进行部分重命名。

  2. 它可以在写入部分寄存器后立即分配。合并uop获取先前的物理寄存器(它将始终是完整的rax/eax写入,或者在标志位的情况下,是完整的状态标志更新,例如由add或合并uop执行的那样),并将其与新的物理寄存器组合成新的物理寄存器。这表明分配器插入它。如果它是由译码器插入的,那么分配器可以在不知道先前的RAT指针的情况下在不同的周期中分配该uop。

  3. 它可以在从RAT具有统一状态的寄存器进行读取之前立即分配。这将意味着RAT单独跟踪rax/eax和ax、al、ah。在这种情况下,需要合并的2个物理寄存器来自RAT。

优化手册暗示了后两种情况之一:“在每次部分寄存器写入后发生合并uop”(即,写入axalah但不是eax)。

我在谈论你在可能的合并操作实现列表末尾提到的内容。你提到了RAT跟踪RAX与AX、AL和AH分开。但HSW简化了这个过程。在此之前,mov al,...确实避免了对RAX旧值的错误依赖,因此存在一些机制可以跟踪单独重命名的AL和AH,它们都没有对RAX产生错误依赖。(如果我没记错的话,英特尔的优化手册提到Sandybridge在进行RMW操作时选择不重命名AL。但对于只写访问,它将单独重命名它。) - Peter Cordes
如果您从eax中读取,那么先前对ax/al的写入需要退役,以便可以分配ROB条目。RAT将知道这一点,因为当它重命名eax读取时,它将看到当前的al/ax/eax/rax具有8或16位的宽度,因此它会暂停,直到ax/al写入指令的退役阶段使RAT al/ax/eax/rax条目指向带有32位宽度的累加器RRF条目。当ah写入退役时,它写入累加器RRF寄存器(只有一个),并声明其现在的宽度为32位,因为它之前没有任何指令需要退役。 - Lewis Kelsey
如果您读取eax/ax和ah RAT条目时没有32/16位的宽度指示器(即它的宽度为8位),那么它也会停顿,直到最终指向RRF累加器条目。例如,在64位RRF上,当对ah或任何a寄存器进行写入时,RRF累加器始终为64位,您必须等待RAT自动指向它,这发生在由于停顿而没有重命名在飞行之前。 - Lewis Kelsey
在Sandybridge处理器上,对于mov al,1/add eax, 1操作,我们知道(?)一个将AL合并到RAX的uop会被插入,而无需停顿任何操作。即使在现代Intel处理器上,一个将AH合并到RAX的uop似乎也必须独立地在一个周期内发出。Intel的优化手册详细记录了这两种行为。SnB没有RRF。在Core2/Nehalem处理器上,它会暂停前端大约3个周期来插入一个合并的uop,而不是无限期地等待RRF更新。我不知道Intel如何使用每个寄存器仅有2个RAT条目来创建此行为,但这是事实。 - Peter Cordes
1
“3个周期与合并uop”的描述来自Agner Fog的微架构PDF,这是Core 2 / Nehalem相对于Pentium-M和更早版本的改进。他没有说他是如何测量的,但一个实验可以是两个长依赖链(例如imul延迟),一个在另一个的阴影下,后者/较短的链中有部分寄存器停顿。如果它合并了,您不会看到每个循环迭代的总周期增加,但如果它完全停顿直到结果在RRF中,那么也必须等待其他依赖链。(我不知道他在旧的uarches上的“5-6个周期”停顿是最佳情况还是串行化。) - Peter Cordes
显示剩余5条评论

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