在Haswell/Skylake上,局部寄存器的执行方式是怎样的?写入AL似乎与RAX存在虚假依赖关系,并且AH不一致。

50
这个循环在Intel Conroe/Merom上每3个周期运行一次,受到了预期的imul吞吐量瓶颈的影响。但是在Haswell/Skylake上,它以每11个周期运行一次,显然是因为setnz al依赖于最后一个imul
; synthetic micro-benchmark to test partial-register renaming
    mov     ecx, 1000000000
.loop:                 ; do{
    imul    eax, eax     ; a dep chain with high latency but also high throughput
    imul    eax, eax
    imul    eax, eax

    dec     ecx          ; set ZF, independent of old ZF.  (Use sub ecx,1 on Silvermont/KNL or P4)
    setnz   al           ; ****** Does this depend on RAX as well as ZF?
    movzx   eax, al
    jnz  .loop         ; }while(ecx);

如果setnz al依赖于rax,那么3ximul/setcc/movzx序列形成一个循环依赖链。如果不是,则每个setcc/movzx/3ximul链都是独立的,从更新循环计数器的dec中分叉出来。在HSW/SKL上每次迭代测量到的11c完全可以通过延迟瓶颈进行解释:3x3c(imul) + 1c(setcc的读取-修改-写入) + 1c(在同一寄存器内的movzx)。

离题:避免这些(有意的)瓶颈

我正在寻求可理解/可预测的行为来隔离部分寄存器,而不是追求最佳性能。

例如,xor-零/设置标志/setcc 在任何情况下都更好(在本例中,xor eax,eax / dec ecx / setnz al)。 这会打破所有 CPU 上对 eax 的依赖(除了早期 P6 家族如 PII 和 PIII),仍然避免了部分寄存器合并惩罚,并节省了 1c 的 movzx 延迟。 它还在处理寄存器重命名阶段的 xor 零化上比使用一个较少的 ALU uop。 请参见该链接以获取有关使用带有 setcc 的 xor 零化的更多信息。

请注意,AMD、Intel Silvermont/KNL 和 P4 根本不进行部分寄存器重命名。 它只是 Intel P6 家族 CPU 及其后代 Intel Sandybridge-family 的一个特性,但似乎正在逐步淘汰。

很不幸,gcc在(Godbolt编译器浏览器示例)中会使用cmp / setcc al / movzx eax,al,而本可以使用xor代替movzx。然而,clang会使用xor-zero/cmp/setcc,除非你将多个布尔条件结合起来,例如count += (a==b) | (a==~b)

xor/dec/setnz版本在Skylake、Haswell和Core2上每次迭代运行速度为3.0c(受到imul吞吐量的瓶颈)。在除了PPro/PII/PIII/早期Pentium-M之外的所有乱序CPU上,xor-清零可以打破对eax旧值的依赖(在这些CPU上,它仍然避免部分寄存器合并惩罚,但不会打破dep)。Agner Fog的微架构指南描述了这一点。将xor清零替换为mov eax,0会使其在Core2上每4.78个周期执行一次:当imul在setnz al之后读取eax时,需要2-3c的停顿(在前端?)来插入一个部分寄存器合并uop
此外,我使用了movzx eax,al,它与mov-elimination一样无效,就像mov rax,rax一样。(IvB、HSW和SKL可以在0延迟下重命名movzx eax,bl,但Core2不能)。这使得Core2/SKL上的所有内容都相等,除了部分寄存器的行为。

Core2的行为与Agner Fog的微架构指南一致,但HSW/SKL的行为则不同。从Skylake的第11.10节开始,以前的Intel uarchs也是如此:

为了消除错误依赖关系,通用寄存器的不同部分可以存储在不同的临时寄存器中。

不幸的是,他没有时间为每个新的uarch进行详细的测试以重新测试假设,因此这种行为变化被忽略了。

Agner确实描述了一个合并的uop被插入(无需停顿)来处理Sandybridge到Skylake上的high8寄存器(AH / BH / CH / DH),并且在SnB上的low8 / low16。 (我很抱歉过去传播了错误的信息,并说Haswell可以免费合并AH。 我太快速地浏览了Agner的Haswell部分,没有注意到高8个寄存器的后面一段话。 如果您看到我在其他帖子上发表的错误评论,请告诉我,这样我就可以删除它们或添加更正。 我会尽力找到并编辑我回答过这些问题的答案。)


我的实际问题是:Skylake上的部分寄存器到底如何工作?

从IvyBridge到Skylake,包括high8额外延迟,所有东西都一样吗?

Intel的优化手册没有具体说明哪些CPU有什么假依赖关系(尽管它提到了一些CPU有这些关系),并且遗漏了像读取AH/BH/CH/DH(high8寄存器)一样的事情,即使它们没有被修改也会增加额外的延迟。

如果有任何P6家族(Core2/Nehalem)的行为不在Agner Fog的微架构指南中描述,那也很有趣,但我应该将这个问题的范围限制在Skylake或Sandybridge家族。


我的Skylake测试数据,是通过在一个运行1亿或10亿次迭代的小dec ebp/jnz循环中放置%rep 4短序列来获得的。我使用Linux perf以与这里的答案相同的方式,在相同的硬件(桌面Skylake i7 6700k)上测量周期。

除非另有说明,否则每个指令都作为1个融合域uop运行,使用ALU执行端口。(使用ocperf.py stat -e ...,uops_issued.any,uops_executed.thread进行测量)。这可以检测到(缺少)mov消除和额外合并uop。

"每周期4个"的情况是对无限展开情况的推断。循环开销占用了一些前端带宽,但任何优于每周期1个的情况都表明寄存器重命名避免了写后写输出依赖性,并且该uop在内部不被处理为读取修改写入。

仅写入AH:防止循环从回送缓冲区(也称为Loop Stream Detector(LSD))执行。在HSW上,lsd.uops的计数完全为0,在SKL上很小(约为1.8k),并且不随循环迭代次数而变化。可能这些计数来自某个内核代码。当循环从LSD运行时,lsd.uops ~= uops_issued,误差在测量噪声范围内。一些循环会在LSD或无LSD之间交替运行(例如,当它们可能无法适应uop缓存时,如果解码开始位置错误),但我在测试中没有遇到过这种情况。

  • 重复的mov ah, bh和/或mov ah, bl每个周期运行4次。它需要一个ALU uop,因此不能像mov eax, ebx一样被消除。
  • 重复的mov ah, [rsi]每个周期运行2次(加载吞吐量瓶颈)。
  • 重复的mov ah, 123每个周期运行1次。(循环内的dep-breaking xor eax,eax去除了瓶颈。)
  • 重复的setz ahsetc ah每个周期运行1次。(dep-breaking xor eax,eax使其在setcc和循环分支上瓶颈为p06吞吐量。)

    为什么使用通常会使用ALU执行单元的指令写入ah会对旧值产生虚假依赖,而mov r8,r/m8不会(对于reg或memory src)?(那么mov r/m8,r8呢?无论使用这两个操作码中的哪一个进行reg-reg移动都没有关系,对吗?)

  • 重复的add ah, 123每个周期运行1次,如预期。

  • 重复的add dh,cl每个周期运行1次。
  • 重复的add dh,dh每个周期运行1次。
  • 重复的add dh,ch每个周期运行0.5次。当[ABCD]H是“干净”的时候(在这种情况下,RCX根本没有被修改过),读取它们是特殊的。
术语:所有这些都使AH(或DH)“脏”,即需要在读取寄存器的其余部分时(或在其他某些情况下)与合并uop合并。也就是说,如果我理解正确,AH与RAX分别重命名。 “清洁”是相反的。有许多方法可以清除脏寄存器,最简单的方法是inc eaxmov eax,esi
仅写入AL:这些循环确实从LSD运行:uops_issue.any〜=lsd.uops
  • 重复的mov al, bl每个周期运行一次。偶尔的dep-breaking xor eax,eax可以让OOO执行瓶颈在uop吞吐量上,而不是延迟。
  • 重复的mov al, [rsi]以微融合ALU+load uop的形式每个周期运行1次。(uops_issued=4G + 循环开销,uops_executed=8G + 循环开销)。在一组4个之前进行dep-breaking xor eax,eax可以让它瓶颈在每个时钟2个loads上。
  • 重复的mov al, 123每个周期运行1次。
  • 重复的mov al, bh每2个周期运行0.5次。(每2个周期1次)。读取[ABCD]H是特殊的。
  • xor eax,eax + 6x mov al,bh + dec ebp/jnz:每次迭代2c,瓶颈在前端每个时钟4个uops。
  • 重复的add dl, ch每2个周期运行0.5次。(每2个周期1次)。读取[ABCD]H显然会为dl创建额外的延迟。
  • 重复的add dl, cl每个周期运行1次。

我认为对一个低8位寄存器的写操作行为类似于RMW混合到完整寄存器中,就像add eax, 123一样,但如果ah是脏的,则不会触发合并。因此(除了忽略AH合并外),它的行为与根本不执行部分寄存器重命名的CPU相同。看起来AL从未与RAX分别重命名过?

  • inc al/inc ah对可以并行运行。
  • mov ecx, eax如果ah是"dirty",则插入合并uop,但实际的mov被重命名。这是Agner Fog描述IvyBridge及更高版本的情况。
  • 重复的movzx eax, ah每2个周期运行一次。(在写入完整寄存器后读取高8个寄存器具有额外的延迟。)
  • movzx ecx, al具有零延迟,在HSW和SKL上不占用执行端口。(就像Agner Fog为IvyBridge描述的那样,但他说HSW不会重命名movzx)。
  • movzx ecx, cl具有1c延迟并占用执行端口。(mov-elimination从不适用于same,same情况,仅适用于不同的体系结构寄存器之间。)

    每次迭代都插入合并uop的循环无法从LSD(循环缓冲区)运行?

我认为AL / AH / RAX与B *,C *,DL / DH / RDX没有任何特殊之处。我已经测试了一些其他寄存器中的部分寄存器(尽管我主要为了一致性而显示AL/AH),并且从未注意到任何区别。

如何用一个合理的模型来解释微体系结构内部运作的所有观察结果?

相关: 部分标志位问题与部分寄存器问题不同。请参阅INC指令与ADD 1:有关shr r32,cl的超怪异问题,请参见各个版本(甚至在Core2 / Nehalem上进行shr r32,2:不要从除1以外的移位读取标志)。

另请参阅某些CPU上ADC / SBB和INC / DEC在紧密循环中的问题,了解adc循环中部分标志位的情况。


1
以问答形式撰写这篇文章真的很费劲,比实际实验还要花费更多时间。但我认为我成功地创造了一个问题,可以被其他人有用地回答,而且问题也不会太简单。虽然我不确定把大部分内容放到答案中是否会更好,但我希望问题标题能够概括重要部分。 - Peter Cordes
你的辛勤工作确实很有用,它解决了我一些困惑。我不知道HSW/SKL在ALU操作写入部分寄存器后不再发出合并uop。我2020年5月份的手册上写着:“从Sandy Bridge微架构开始和所有后续的Intel Core微架构,部分寄存器访问是通过插入一个微操作来处理的,该微操作将部分寄存器与以下情况中的完整寄存器合并”(我强调)。它没有澄清这适用于MOV但不适用于其他指令。 - icecreamsword
为什么重复执行 mov al, 123 每个周期只运行1次,但是 movl eax, 123 重复运行每个迭代需要4个周期?不用担心,这是因为 mov al, 123 不具有依赖性破坏。 - Noah
2个回答

36

欢迎提供其他关于Sandybridge和IvyBridge的详细信息,但我无法访问这些硬件。


我还没有发现HSW和SKL之间的部分寄存器行为差异。在Haswell和Skylake上,到目前为止我测试过的所有内容都支持这个模型:

AL从未与RAX(或r15b与r15)分别重命名。因此,如果您从不触及高8个寄存器(AH/BH/CH/DH),则一切都像在没有部分寄存器重命名的CPU上一样。

对AL的只写访问合并到RAX中,并依赖于RAX。对于AL的加载,这是一个微合并的ALU+load uop,执行在p0156上,这是真正每次写入时合并的最有力证据,而不仅仅是像Agner所推测的那样做一些花式的双重记账。

Agner(和Intel)说Sandybridge可能需要一个合并的uop来处理AL,因此它可能与RAX分别重命名。对于SnB,英特尔优化手册(第3.5.2.4节部分寄存器停顿)说:

SnB(不一定是后续 uarches)在以下情况下插入合并 uop:
  • 在写入寄存器 AH、BH、CH 或 DH 中的一个之后,并在随后读取相同寄存器的 2、4 或 8 字节形式之前。在这些情况下,会插入合并微操作。插入需要消耗一个完整的分配周期,在此期间其他微操作无法分配。
  • 在目标寄存器为 1 或 2 字节的微操作之后,该微操作不是指令的源(或寄存器的较大形式),并在随后读取相同寄存器的 2、4 或 8 字节形式之前。在这些情况下,合并微操作是流程的一部分

我认为他们的意思是在 SnB 上,add al,bl 将 RMW 整个 RAX 而不是单独重命名它,因为其中一个源寄存器是(RAX 的一部分)。我猜想这不适用于像 mov al, [rbx + rax] 这样的加载;在寻址模式中的 rax 可能不算作源。

我还没有测试 HSW/SKL 上高 8 合并 uops 是否仍然需要单独发出/重命名。这将使前端影响等同于 4 个 uops(因为这是发出/重命名流水线宽度)。

  • 如果不使用EAX/RAX,没有办法打破与AL相关的依赖关系。 xor al,almov al,0都无济于事。
  • movzx ebx, al具有零延迟(已重命名),不需要执行单元。(即在HSW和SKL上可以执行移动消除)。如果AH是脏的,则会触发合并,我想这对于它在没有ALU的情况下工作是必要的。很可能英特尔在引入mov-elimination的同一微架构中放弃了low8重命名。(Agner Fog的微架构指南在这里有一个错误,即零扩展移动在HSW或SKL上不会被消除,只有IvB。)
  • movzx eax, al在重命名时不会被消除。 Intel上的mov-elimination从不适用于same,same。即使它不必扩展任何内容,mov rax,rax也不会被消除。(虽然给它特殊的硬件支持没有意义,因为它只是一个无操作,不像mov eax,eax)。总之,在进行零扩展时,请优先将其移动到两个单独的架构寄存器之间,无论是使用32位mov还是8位movzx
  • movzx eax, bx在HSW或SKL上重命名时不会被消除。它具有1c延迟并使用ALU uop。英特尔的优化手册仅提到了8位movzx的零延迟(并指出movzx r32, high8从未被重命名)。

High-8寄存器可以与其他寄存器分开重命名,并且确实需要合并微操作。

  • 使用mov ah, reg8mov ah, [mem8]只有对ah的写入权限,不依赖于旧值。这两个指令在32位版本中通常不需要ALU uop。(但是mov ah, bl不能被消除;它确实需要一个p0156 ALU uop,所以可能是巧合)。
  • 对AH进行RMW(如inc ah)会使其变脏。
  • setcc ah依赖于旧的ah,但仍然会使其变脏。我认为mov ah, imm8也是一样的,但没有测试过许多边角情况。

    (未解释:涉及setcc ah的循环有时可以从LSD运行,参见本帖子末尾的rcr循环。也许只要ah在循环结束时是干净的,它就可以使用LSD?)。

    如果ah是脏的,setcc ah会合并到重命名的ah中,而不是强制合并到rax中。例如:%rep 4 (inc al / test ebx,ebx / setcc ah / inc al / inc ah)不生成合并uops,并且仅运行约8.7c (由于ah的uop冲突造成的8个inc al的延迟。此外,还有inc ah / setcc ah依赖链)。

    我想这里发生的事情是setcc r8总是实现为读取-修改-写入。英特尔可能认为没有必要有一个只写setcc uop来优化setcc ah的情况,因为编译器生成的代码很少setcc ah。(但是请参见问题中的godbolt链接:clang4.0与-m32将这样做。)

  • 读取AX、EAX或RAX会触发合并uop(占用前端问题/重命名带宽)。可能RAT(寄存器分配表)跟踪R[ABCD]X的高8位脏状态,即使在写入AH之后,AH数据也存储在与RAX不同的物理寄存器中。即使在写入AH和读取EAX之间有256个NOP,仍会触发额外的合并uop。(在SKL上,ROB大小为224,因此这保证了mov ah, 123已经退休)。使用uops_issued/executed perf计数器检测到,它们清楚地显示了差异。

  • 对AL的读取和/或写入不会强制合并,因此AH可以保持脏(并在单独的依赖链中独立使用)。(例如:add ah, cl / add al, dl可以以每秒1个时钟周期运行(瓶颈在于add延迟)。

  • 对EAX/RAX的只写(如lea eax, [rsi+rcx]xor eax,eax)清除AH-dirty状态(


    使AH寄存器变脏可以防止循环从LSD(循环缓冲区)运行,即使没有合并的uops。LSD是当CPU在队列中回收uops并馈送到发出/重命名阶段时的情况。(称为IDQ)。
    插入合并uops有点像为堆栈引擎插入堆栈同步uops。英特尔优化手册指出,SnB的LSD无法运行具有不匹配的push/pop的循环,这是有道理的,但它暗示着它可以运行具有平衡的push/pop的循环。但这不是我在SKL上看到的:即使是平衡的push/pop也会阻止从LSD运行(例如:push rax/pop rdx/times 6 imul rax,rdx)。(SnB的LSD和HSW/SKL之间可能存在真正的差异: SnB可能只是在IDQ中锁定uops而不是多次重复它们,因此一个5-uop循环需要2个周期来发出,而不是1.25个周期.)总之,似乎当高8位寄存器变脏或包含堆栈引擎uops时,HSW/SKL无法使用LSD。

    这种行为可能与SKL勘误相关:

    SKL150: 使用AH/BH/CH/DH寄存器的短循环可能会导致不可预测的系统行为

    问题:在复杂的微架构条件下,使用AH、BH、CH或DH寄存器及其对应的宽寄存器(例如AH的RAX、EAX或AX)的少于64条指令的短循环可能会导致不可预测的系统行为。只有当同一物理处理器上的两个逻辑处理器都处于活动状态时才会发生。

    这也可能与英特尔优化手册中的声明有关,即SnB至少必须在一个周期内发出/重命名一个AH合并uop。这是前端的一个奇怪的差异。

    我的Linux内核日志显示microcode: sig=0x506e3, pf=0x2, revision=0x84。 Arch Linux的intel-ucode软件包只提供更新,您需要编辑配置文件才能实际加载它。所以我的Skylake测试是在i7-6700k上进行的,微代码版本为0x84,其中不包括SKL150的修复程序。在我测试的每种情况下,它都与Haswell的行为相匹配,如果我没记错的话。(例如,Haswell和我的SKL都可以运行从LSD循环的setne ah/add ah,ah/rcr ebx,1/mov eax,ebx)。我启用了HT(这是SKL150显现的前提条件),但我是在一个基本空闲的系统上进行测试的,因此我的线程有核心的控制权。
    通过更新微码,LSD在任何时候都完全禁用,而不仅仅是在部分寄存器处于活动状态时禁用。即使对于实际程序而言,lsd.uops始终为零,而不仅仅是对于合成循环。硬件缺陷(而不是微码缺陷)通常需要禁用整个功能来修复。这就是为什么SKL-avx512(SKX)被报告为没有环回缓冲区的原因。幸运的是,这不是一个性能问题:SKL相对于Broadwell的增加uop-cache吞吐量几乎总是可以跟上issue/rename的步伐。

    额外的AH/BH/CH/DH延迟:

    • 当AH没有被修改时(单独重命名),读取AH会为两个操作数增加一个周期的延迟。例如,add bl, ah从输入BL到输出BL的延迟为2c,因此即使RAX和AH不是关键路径的一部分,它也可以添加延迟到关键路径中。(我之前看过在Skylake上使用向量延迟的另一个操作数的这种额外延迟,其中int/float延迟“污染”寄存器永久存在。待办事项:写出来。)

    这意味着使用 movzx ecx, al / movzx edx, ah 拆包字节会比 movzx/shr eax,8/movzx 有额外的延迟,但吞吐量更好。

    • 当AH被修改时,读取AH不会增加任何延迟。(add ah,ahadd ah,dh/add dh,ah 每个加法都有1c的延迟)。我还没有进行过大量测试以确认在许多边缘情况下是否成立。

      假设:脏高8值存储在物理寄存器的底部。读取干净的高8需要移位以提取位 [15:8],但读取脏高8只需像普通的8位寄存器读取一样获取物理寄存器的位 [7:0]。

    额外的延迟并不意味着吞吐量降低。即使所有add指令都有2个时钟周期的延迟(从读取未被修改的DH开始),该程序仍可在每2个时钟周期运行1个迭代。
    global _start
    _start:
        mov     ebp, 100000000
    .loop:
        add ah, dh
        add bh, dh
        add ch, dh
        add al, dh
        add bl, dh
        add cl, dh
        add dl, dh
    
        dec ebp
        jnz .loop
    
        xor edi,edi
        mov eax,231   ; __NR_exit_group  from /usr/include/asm/unistd_64.h
        syscall       ; sys_exit_group(0)
    

     Performance counter stats for './testloop':
    
         48.943652      task-clock (msec)         #    0.997 CPUs utilized          
                 1      context-switches          #    0.020 K/sec                  
                 0      cpu-migrations            #    0.000 K/sec                  
                 3      page-faults               #    0.061 K/sec                  
       200,314,806      cycles                    #    4.093 GHz                    
       100,024,930      branches                  # 2043.675 M/sec                  
       900,136,527      instructions              #    4.49  insn per cycle         
       800,219,617      uops_issued_any           # 16349.814 M/sec                 
       800,219,014      uops_executed_thread      # 16349.802 M/sec                 
             1,903      lsd_uops                  #    0.039 M/sec                  
    
       0.049107358 seconds time elapsed
    

    一些有趣的测试循环体:

    %if 1
         imul eax,eax
         mov  dh, al
         inc dh
         inc dh
         inc dh
    ;     add al, dl
        mov cl,dl
        movzx eax,cl
    %endif
    
    Runs at ~2.35c per iteration on both HSW and SKL.  reading `dl` has no dep on the `inc dh` result.  But using `movzx eax, dl` instead of `mov cl,dl` / `movzx eax,cl` causes a partial-register merge, and creates a loop-carried dep chain.  (8c per iteration).
    
    
    %if 1
        imul  eax, eax
        imul  eax, eax
        imul  eax, eax
        imul  eax, eax
        imul  eax, eax         ; off the critical path unless there's a false dep
    
      %if 1
        test  ebx, ebx          ; independent of the imul results
        ;mov   ah, 123         ; dependent on RAX
        ;mov  eax,0           ; breaks the RAX dependency
        setz  ah              ; dependent on RAX
      %else
        mov   ah, bl          ; dep-breaking
      %endif
    
        add   ah, ah
        ;; ;inc   eax
    ;    sbb   eax,eax
    
        rcr   ebx, 1      ; dep on  add ah,ah  via CF
        mov   eax,ebx     ; clear AH-dirty
    
        ;; mov   [rdi], ah
        ;; movzx eax, byte [rdi]   ; clear AH-dirty, and remove dep on old value of RAX
        ;; add   ebx, eax          ; make the dep chain through AH loop-carried
    %endif
    

    setcc版本(带有%if 1)具有20个时钟周期的循环延迟,并且即使它具有setcc ahadd ah,ah,也是从LSD运行的。

    00000000004000e0 <_start.loop>:
      4000e0:       0f af c0                imul   eax,eax
      4000e3:       0f af c0                imul   eax,eax
      4000e6:       0f af c0                imul   eax,eax
      4000e9:       0f af c0                imul   eax,eax
      4000ec:       0f af c0                imul   eax,eax
      4000ef:       85 db                   test   ebx,ebx
      4000f1:       0f 94 d4                sete   ah
      4000f4:       00 e4                   add    ah,ah
      4000f6:       d1 db                   rcr    ebx,1
      4000f8:       89 d8                   mov    eax,ebx
      4000fa:       ff cd                   dec    ebp
      4000fc:       75 e2                   jne    4000e0 <_start.loop>
    
     Performance counter stats for './testloop' (4 runs):
    
           4565.851575      task-clock (msec)         #    1.000 CPUs utilized            ( +-  0.08% )
                     4      context-switches          #    0.001 K/sec                    ( +-  5.88% )
                     0      cpu-migrations            #    0.000 K/sec                  
                     3      page-faults               #    0.001 K/sec                  
        20,007,739,240      cycles                    #    4.382 GHz                      ( +-  0.00% )
         1,001,181,788      branches                  #  219.276 M/sec                    ( +-  0.00% )
        12,006,455,028      instructions              #    0.60  insn per cycle           ( +-  0.00% )
        13,009,415,501      uops_issued_any           # 2849.286 M/sec                    ( +-  0.00% )
        12,009,592,328      uops_executed_thread      # 2630.307 M/sec                    ( +-  0.00% )
        13,055,852,774      lsd_uops                  # 2859.456 M/sec                    ( +-  0.29% )
    
           4.565914158 seconds time elapsed                                          ( +-  0.08% )
    

    未经解释:它从LSD运行,即使它会让AH变脏。(至少我认为是这样。TODO:尝试在mov eax,ebx清除之前添加一些与eax有关的指令。)

    但是使用mov ah,bl,它在HSW / SKL上每次迭代运行5.0c(通过imul瓶颈)。(注释掉的存储/重新加载也可以工作,但SKL具有比HSW更快的存储转发,并且它是variable-latency...)

     #  mov ah, bl   version
     5,009,785,393      cycles                    #    4.289 GHz                      ( +-  0.08% )
     1,000,315,930      branches                  #  856.373 M/sec                    ( +-  0.00% )
    11,001,728,338      instructions              #    2.20  insn per cycle           ( +-  0.00% )
    12,003,003,708      uops_issued_any           # 10275.807 M/sec                   ( +-  0.00% )
    11,002,974,066      uops_executed_thread      # 9419.678 M/sec                    ( +-  0.00% )
             1,806      lsd_uops                  #    0.002 M/sec                    ( +-  3.88% )
    
       1.168238322 seconds time elapsed                                          ( +-  0.33% )
    

    请注意,它不再从LSD运行。


7
这是[x86]调查新闻的最佳范例。谢谢! - Iwillnotexist Idonotexist
1
@BeeOnRope:刚刚仔细核对了一下:Arch Linux的 intel-ucode 软件包在安装时不会自动启用微码更新;您需要编辑引导加载程序配置文件。我在测试SKL时没有修复,但一个核心上没有两个线程。我不记得注意到AH / LSD在HSW和SKL上有所不同的结果。等我下次重新启动后有时间,我会重新测试并查看是否有任何新情况,SKL将不使用LSD。 - Peter Cordes
1
在ICL方面,某些事情可能已经发生了巨大的变化。请参见此ICL Instlat转储。它显示许多指令的r8目标突然从通常的0.25c或其他操作取决的速度提高到1c。我不知道这些测试中使用了哪些高低寄存器的混合,但无论如何,这似乎是一个很大的变化。 - BeeOnRope
1
@BeeOnRope:好问题,我刚测试了一下,它没有被消除。我更新了那个要点。它需要一个p0156 uop。一个包含7个mov ah, bl和一个dec/jnz的循环在2.001c/迭代运行,并且基本上饱和了所有4个ALU端口,证明它确实是p0156而不是假依赖。 - Peter Cordes
1
@Noah:如果它在重要的循环内部,并且会在 Nehalem 或更早期(使前端停顿约 3 个周期)导致性能灾难,我认为我们仍然应该避免使用它。仍然有 Nehalem 甚至 Core2 系统存在。特别是如果收益只是代码大小(以字节为单位),甚至不是 uops。在使用 AVX 的函数中,这排除了 Pre-SnB,而 SnB 本身可以处理它。 (现代 Pentium / Celeron Pre-IceLake 没有 AVX1 或 2,因此它们和 Tremont 等是最近使用非 AVX 版本函数的 CPU。Bulldozer 具有 AVX。除非在不传递 AVX 的 VM 中。) - Peter Cordes
显示剩余21条评论

9
更新:可能的证据表明,IvyBridge仍然将low16 / low8寄存器与完整寄存器分别重命名,就像Sandybridge一样,但不像Haswell及以后的版本。
从SnB和IvB的InstLatX64结果显示movsx r16, r8的吞吐量为0.33c(正如预期的那样,movsx永远不会被消除,在Haswell之前只有3个ALU)。
但显然,InstLat的movsx r16, r8测试会在Haswell / Broadwell / Skylake上出现1c的瓶颈(请参见instlat github上的此错误报告)。可能是通过写入相同的体系结构寄存器,创建一条合并链。

(在我的Skylake上,使用单独的目标寄存器执行该指令的实际吞吐量为0.25c。测试了7个movsx指令,写入eax..edi和r10w/r11w,所有指令均从cl读取。而一个dec ebp/jnz作为循环分支,使其成为一个连续的8个uop循环。)

如果我猜测正确,关于在IvB之后的CPU上产生1c吞吐量结果的原因,是执行一组movsx dx, al。只有在将dx与RDX分开重命名而不是合并的CPU上,才能以超过1 IPC的速度运行。因此,我们可以得出结论,IvB实际上仍然会将low8 / low16寄存器与完整寄存器分别重命名,并且直到Haswell才放弃这种做法。(但是这里有些可疑:如果这个解释是正确的,那么我们应该在AMD上看到相同的1c吞吐量,因为AMD不会对部分寄存器进行重命名。但我们没有看到,详见下文。)

movsx r16, r8(和movzx r16, r8)测试的吞吐量约为0.33c。

Haswell结果显示神秘的0.58c吞吐量,用于movsx/zx r16,r8

其他早期和晚期的Haswell(和CrystalWell)/ Broadwell / Skylake测试结果,这两个测试的吞吐量均为1.0c。

  • HSW 于2013年6月5日发布的版本为4.1.570.0,BDW 于2018年10月12日发布的版本为4.3.15787.0,BDW 于2017年3月17日发布的版本为4.3.739.0。

如我在Github上链接的InstLat问题中所报告的那样,“latency”数字对于movzx r32,r8忽略了mov消除,可能像movzx eax,al 一样进行测试。

更糟糕的是,新版本的InstLatX64使用单独寄存器版本的测试,例如MOVSX r1_32,r2_8,显示出低于1个周期的延迟数字,例如Skylake上该MOVSX的0.3c。这是完全没有意义的; 我进行了测试以确保。

MOVSX r1_16,r2_8测试显示1c的延迟,因此显然他们只是测量输出(错误)依赖性的延迟。 (32位及更宽输出不存在此类依赖关系)。 但是,在 Sandybridge 上MOVSX r1_16, r2_8 测试也显示出了 1c 的延迟!因此,也许我的理论关于 movsx r16, r8 测试告诉我们什么是错误的。

在Ryzen上(AIDA64版本为4.3.781.0,发布于2018年2月21日),我们知道它根本不执行任何部分寄存器重命名,如果该测试确实重复写入相同的16位寄存器,结果将不会显示预期的1c吞吐量效果。我也没有在任何旧的AMD CPU上找到它,例如K10或Bulldozer系列,使用旧版本的InstLatX64。

## Instlat Zen tests of ... something?
  43 X86     :MOVSX r16, r8                L:   0.28ns=  1.0c  T:   0.11ns=  0.40c
  44 X86     :MOVSX r32, r8                L:   0.28ns=  1.0c  T:   0.07ns=  0.25c
  45 AMD64   :MOVSX r64, r8                L:   0.28ns=  1.0c  T:   0.12ns=  0.43c
  46 X86     :MOVSX r32, r16               L:   0.28ns=  1.0c  T:   0.12ns=  0.43c
  47 AMD64   :MOVSX r64, r16               L:   0.28ns=  1.0c  T:   0.13ns=  0.45c
  48 AMD64   :MOVSXD r64, r32              L:   0.28ns=  1.0c  T:   0.13ns=  0.45c

IDK为什么吞吐量并不是全部都是0.25,这看起来很奇怪。这可能是0.58c Haswell吞吐效应的一个版本。MOVZX数字相同,没有前缀版本的吞吐量为0.25,读取R8并写入R32。也许在获取/解码大型指令时存在瓶颈?但是movsx r32,r16与movsx r32,r8的大小相同。对于单独的寄存器测试,显示与英特尔相同的模式,只有必须合并的那个测试具有1c延迟。MOVZX也是相同的。
## Instlat Zen separate-reg tests
2252 X86     :MOVSX r1_16, r2_8            L:   0.28ns=  1.0c  T:   0.08ns=  0.28c
2253 X86     :MOVSX r1_32, r2_8            L:   0.07ns=  0.3c  T:   0.07ns=  0.25c
2254 AMD64   :MOVSX r1_64, r2_8            L:   0.07ns=  0.3c  T:   0.07ns=  0.25c
2255 X86     :MOVSX r1_32, r2_16           L:   0.07ns=  0.3c  T:   0.07ns=  0.25c

挖掘机的结果也非常相似,但当然吞吐量更低。确认Zen+具有预期的0.25c吞吐量(和1c延迟)https://www.uops.info/table.html,适用于MOVSX_NOREX (R16, R8),这与Instlat在其单独寄存器测试中发现的结果相同。也许InstLat对于MOVSX r16,r8(而不是MOVSX r1_16,r2_8)的吞吐量测试仅使用了2或3个依赖链,这对于现代CPU来说不足够?或者偶尔打破依赖链以便OoO执行可以重叠一些?

1
看起来Zen 3的行为在这里有所改变。Zen 2及以下版本似乎对于add r8,r8具有0.25的inv吞吐量,但是Zen 3是1.0。还有其他几个变化。显然,Zen 3在其ALU中更加异构(即,在Zen 1,2中,大多数操作都可以在所有4个ALU上执行,但在Zen 3中,许多操作只能在较少的ALU上执行,因此也许字节操作以这种方式被降级了?)。还有一些可疑的结果,例如CMP r8,r8显示为0.02的逆吞吐量(即每个周期可以执行50个这样的操作)。 - BeeOnRope
5900X结果链接 - BeeOnRope

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