x86的MOV指令真的可以“免费”吗?为什么我无法复现这个结果?

47

我看到很多人声称,在x86中,由于寄存器重命名,MOV指令可以是免费的。

但我怎么也无法在任何一个测试用例中验证这一点。每个测试用例都证明了它是错误的。

例如,这是我在使用Visual C++编译的代码:

#include <limits.h>
#include <stdio.h>
#include <time.h>

int main(void)
{
    unsigned int k, l, j;
    clock_t tstart = clock();
    for (k = 0, j = 0, l = 0; j < UINT_MAX; ++j)
    {
        ++k;
        k = j;     // <-- comment out this line to remove the MOV instruction
        l += j;
    }
    fprintf(stderr, "%d ms\n", (int)((clock() - tstart) * 1000 / CLOCKS_PER_SEC));
    fflush(stderr);
    return (int)(k + j + l);
}

以下是循环的汇编代码(随意使用任何方式生成;显然您不需要使用Visual C++):

LOOP:
    add edi,esi
    mov ebx,esi
    inc esi
    cmp esi,FFFFFFFFh
    jc  LOOP

现在我运行这个程序多次,发现当移除MOV指令时,结果有一个相当一致的2%差异:

Without MOV      With MOV
  1303 ms         1358 ms
  1324 ms         1363 ms
  1310 ms         1345 ms
  1304 ms         1343 ms
  1309 ms         1334 ms
  1312 ms         1336 ms
  1320 ms         1311 ms
  1302 ms         1350 ms
  1319 ms         1339 ms
  1324 ms         1338 ms

那出了什么问题呢?为什么MOV指令不是“免费”的呢?这个循环对于x86来说太复杂了吗?
是否有一个唯一的示例可以证明MOV指令像人们所声称的那样是“免费”的吗?
如果有,它是什么?如果没有,为什么每个人都声称MOV指令是“免费”的呢?


6
“freeness” 指的是延迟,这里你没有进行测量。此外,其中的 2% 相比于一个周期来说显著较少,因此会出现“奇怪影响”。 - harold
2
“完全删除”到底是什么意思呢?显然在解码之前它不能被删除,因为还不知道它是什么。毫不奇怪的是,在重命名期间,这个重命名技巧最多只能在重命名时删除mov指令,而且并不总是有效。只要存在,mov指令就不能完全自由。 - harold
9
你增加了25%的指令,但它只慢了2%。你不能用“似乎没有MOV消除”来解释这一点。2%的差异需要另一个解释,比如核心过热并降低速度。 - Hans Passant
11
寄存器重命名有效地消除了后端中的 MOV,这意味着它由 0 个微操作组成,不占用执行端口,且延迟为0。然而,该指令本身仍需要解码,这不是免费的。此外,它会占用代码中的空间,这意味着缓存中的空间。因此,MOV 从来都不是真正免费的,因为在前端存在一些成本,但在做某些有意义的操作的更大的代码块的上下文中通常是 有效 免费的。执行速度差异为2%显然比一个时钟周期要小得多,这超出人们最初的期望。 - Cody Gray
5
已经消除的MOV指令在ROB中占用空间,直到其退役(类似于异或清零指令甚至是NOP指令),在英特尔硬件上(没有分支预测错误的情况下,“uops_retired.retire_slots”几乎完全匹配“uops_issued.any”)。 我的认知模型是它们以已执行、准备好退役的状态进入ROB(融合域),并且没有未合并的域uop被发射到RS(调度器)中。可能有一些非平凡的事情与没有uop可供指令退役有关,也许是关于更新RIP或回滚错误预测的问题... - Peter Cordes
显示剩余7条评论
2个回答

68

对于前端来说,寄存器复制从未是免费的,只有在以下CPU的问题/重命名阶段中被消除(零延迟)后端执行时才会被消除:

  • AMD Bulldozer家族适用于XMM向量寄存器,不适用于整数。
  • AMD Zen家族适用于整数和XMM向量寄存器。(Zen2及更高版本还包括YMM)
    (有关BD / Zen 1中YMM低/高半部分的详细信息,请参见Agner Fog's微体系结构指南)
  • Intel Ivy Bridge及更高版本适用于整数和向量寄存器(除了MMX)
  • Intel Goldmont及更高版本的低功耗CPU:XMM和整数
    (包括Alder Lake E-cores整数/XMM但不包括YMM)
  • 不适用于Intel Ice Lake:一个微码更新禁用了通用整数寄存器的寄存器重命名,以解决错误问题。 XMM/YMM/ZMM重命名仍然有效。 Tiger Lake也受到影响,但Rocket Lake或Alder Lake P-cores不受影响。
    uops.info结果为mov r32, r32 - 请注意,在其有效的CPU上延迟=0。 请注意,它们在Ice Lake和Tiger Lake上显示延迟=1,因此它们在微码更新后重新测试。

你的实验

问题中循环的吞吐量不取决于MOV的延迟,或(在Haswell上)不使用执行单元的好处。

该循环仍然只有4个uops可以发出到乱序后端。(即使mov不需要执行单元,它仍然需要由乱序后端跟踪,但cmp/jc会宏观融合成一个uop)。

自Core 2以来,英特尔CPU的问题宽度为每个时钟周期4个uops,因此在Haswell上,mov不会阻止其以接近每个时钟周期的速度执行一次迭代。它也将在Ivybridge上以每个时钟周期一次的速度运行(通过mov消除),但不会在Sandybridge上运行(没有mov消除)。在SnB上,它大约每1.333个周期运行一次,瓶颈在ALU吞吐量上,因为mov总是需要一个。(SnB/IvB只有三个ALU端口,而Haswell有四个)。

请注意,在重命名阶段对x87 FXCHG(交换st0st1)进行特殊处理已经存在很长时间,比MOV更久。 Agner Fog将FXCHG列为PPro / PII / PIII(第一代P6核心)上的0延迟。
问题中的循环有两个交织的依赖链(add edi,esi 依赖于 EDI 和循环计数器 ESI),这使得它更容易受到不完美调度的影响。与理论预测相比,因为看似无关的指令而导致的2%减速并不罕见,指令顺序的小变化就可以造成这种差异。为了每次迭代正好运行1c,每个周期都需要运行一个INC和一个ADD。由于所有的INC和ADD都依赖于前一次迭代,乱序执行不能通过在单个周期内运行两个来追赶上去。更糟糕的是,ADD依赖于前一个周期的INC,这就是我所说的“交织”,所以在INC dep链中失去一个周期也会使ADD dep链停滞。
此外,预测分支只能在port6上运行,因此任何port6没有执行cmp / jc的周期都是吞吐量损失的周期。每当INC或ADD窃取port6上的一个周期而不是在端口0、1或5上运行时,就会发生这种情况。我不知道这是否是罪魁祸首,还是INC / ADD dep链本身失去了周期的问题,或者两者都有可能。 增加额外的MOV指令不会增加任何执行端口的压力,假设它被完全消除,但它会阻止前端超越后端执行单元。(循环中只有3个4个uops需要执行单元,您的Haswell CPU可以在其4个ALU端口0、1、5和6上运行INC和ADD。因此瓶颈是:
  • 前端每个时钟最大吞吐量为4个uops。(没有MOV的循环只有3个uops,因此前端可以超前运行)。
  • 每个时钟的taken-branch吞吐量为1个。
  • 涉及esi的依赖链(每个时钟的INC延迟)
  • 涉及edi的依赖链(每个时钟的ADD延迟,并且还依赖于前一次迭代的INC)

没有MOV指令,前端可以每个时钟周期发出循环的三个uops,直到乱序后端满为止。 (据我所知,它在循环缓冲区(Loop Stream Detector:LSD)中“展开”微小循环,因此具有ABC uops的循环可以以ABCA BCAB CABC ...模式发出。 lsd.cycles_4_uops 的性能计数器证实,当它发出任何uops时,它主要以4个一组发出。)

Intel CPU将uops分配给端口,当它们进入乱序后端时。这个决策基于计数器来跟踪调度器(也称为预约站,RS)中每个端口的已有uops数量。当RS中有大量uops等待执行时,这种方法很有效,并且通常应该避免将INC或ADD调度到端口6。我想这也避免了将INC和ADD调度在时间上从这两个dep链中损失。但是如果RS为空或接近空,计数器将无法阻止ADD或INC从端口6窃取一个周期。

我认为我在这里找到了一些线索,但任何次优的调度都应该让前端赶上并保持后端满负荷。我不认为我们应该期望前端引起足够多的流水线气泡来解释低于最大吞吐量2%的下降,因为微小的循环应该以非常一致的4个时钟吞吐量从循环缓冲区运行。也许还有其他问题。


消除mov带来的益处的真实示例。

我使用lea构建了一个循环,每个时钟周期只有一个mov,创建了一个完美的演示,其中MOV-elimination成功率为100%,或者使用 mov same,same 的成功率为0%,以展示产生的延迟瓶颈。

由于宏合并的dec / jnz是涉及循环计数器的依赖链的一部分,因此不完美的调度无法延迟它。这与cmp / jc“分叉”关键路径依赖链的情况不同,后者会在每次迭代中发生。

_start:
    mov     ecx, 2000000000 ; each iteration decrements by 2, so this is 1G iters
align 16  ; really align 32 makes more sense in case the uop-cache comes into play, but alignment is actually irrelevant for loops that fit in the loop buffer.
.loop:
    mov eax, ecx
    lea ecx, [rax-1]    ; we vary these two instructions

    dec ecx             ; dec/jnz macro-fuses into one uop in the decoders, on Intel
    jnz .loop

.end:
    xor edi,edi    ; edi=0
    mov eax,231    ; __NR_exit_group from /usr/include/asm/unistd_64.h
    syscall        ; sys_exit_group(0)

在英特尔Snb-family上,使用一个或两个组件的LEA寻址模式运行时延为1c(请参见http://agner.org/optimize/以及标签wiki中的其他链接)。
我在Linux上将其构建并作为静态二进制文件运行,因此用户空间的perf计数器仅测量具有可忽略的启动/关闭开销的循环。 (与将perf-counter查询放入程序本身相比,perf stat非常容易)
$ yasm -felf64 -Worphan-labels -gdwarf2 mov-elimination.asm && ld -o mov-elimination mov-elimination.o &&
  objdump -Mintel -drwC mov-elimination &&
  taskset -c 1 ocperf.py stat -etask-clock,context-switches,page-faults,cycles,instructions,branches,uops_issued.any,uops_executed.thread  -r2 ./mov-elimination

Disassembly of section .text:

00000000004000b0 <_start>:
  4000b0:       b9 00 94 35 77          mov    ecx,0x77359400
  4000b5:       66 66 2e 0f 1f 84 00 00 00 00 00        data16 nop WORD PTR cs:[rax+rax*1+0x0]

00000000004000c0 <_start.loop>:
  4000c0:       89 c8                   mov    eax,ecx
  4000c2:       8d 48 ff                lea    ecx,[rax-0x1]
  4000c5:       ff c9                   dec    ecx
  4000c7:       75 f7                   jne    4000c0 <_start.loop>

00000000004000c9 <_start.end>:
  4000c9:       31 ff                   xor    edi,edi
  4000cb:       b8 e7 00 00 00          mov    eax,0xe7
  4000d0:       0f 05                   syscall 

perf stat -etask-clock,context-switches,page-faults,cycles,instructions,branches,cpu/event=0xe,umask=0x1,name=uops_issued_any/,cpu/event=0xb1,umask=0x1,name=uops_executed_thread/ -r2 ./mov-elimination

 Performance counter stats for './mov-elimination' (2 runs):

    513.242841      task-clock:u (msec)       #    1.000 CPUs utilized    ( +-  0.05% )
             0      context-switches:u        #    0.000 K/sec                  
             1      page-faults:u             #    0.002 K/sec                  
 2,000,111,934      cycles:u                  #    3.897 GHz              ( +-  0.00% )
 4,000,000,161      instructions:u            #    2.00  insn per cycle   ( +-  0.00% )
 1,000,000,157      branches:u                # 1948.396 M/sec            ( +-  0.00% )
 3,000,058,589      uops_issued_any:u         # 5845.300 M/sec            ( +-  0.00% )
 2,000,037,900      uops_executed_thread:u    # 3896.865 M/sec            ( +-  0.00% )

   0.513402352 seconds time elapsed                                          ( +-  0.05% )

如预期的那样,循环运行了10亿次(branches ~= 10亿)。超过20亿的额外111k周期是其他测试中也存在的开销,包括没有mov的测试。它不是由于mov消除偶尔失败引起的,但它确实随着迭代计数而增加,因此它不仅仅是启动开销。可能是由于定时器中断,因为如果我没记错的话,Linux perf在处理中断时不会干扰性能计数器,而是让它们继续计数。(perf虚拟化硬件性能计数器,因此即使线程跨CPU迁移,也可以获得每个进程的计数。)此外,共享同一物理核心的兄弟逻辑核心上的定时器中断也会稍微干扰一下事情。
瓶颈是涉及循环计数器的循环依赖链。每1G迭代需要2G个周期,即每次递减需要1个时钟周期。这证实了依赖链的长度为2个周期。只有当mov没有延迟时才可能实现这一点。(我知道这并不能证明没有其他瓶颈。如果您不相信我的断言,即延迟是唯一的瓶颈,它只能证明延迟最多为2个周期。有一个resource_stalls.any性能计数器,但它没有太多选项来分解哪个微架构资源已经耗尽。)

这个循环有3个融合域uop: mov, lea, 和 宏融合的 dec/jnz。3G的 uops_issued.any 计数证实了这一点:它在融合域中计数,即从解码器到退役的所有管道,除了调度器(RS)和执行单元。(宏融合指令对在任何地方都保持单个uop。只有对存储器或ALU+load进行微融合时,ROB中的一个融合域uop跟踪两个未融合域uop的进度。)

2G uops_executed.thread(未融合域)告诉我们所有的mov uop都被消除了(即由发行/重命名阶段处理,并以已执行状态放置在ROB中)。它们仍然占用发行/退役带宽、uop缓存中的空间和代码大小。它们占用ROB中的空间,限制乱序窗口大小。mov指令从来不是免费的。除了延迟和执行端口之外,存在许多可能的微架构瓶颈,其中最重要的通常是前端的4宽度问题率。

在英特尔CPU上,零延迟通常比不需要执行单元更为重要,特别是在Haswell及以后的处理器中,其中有4个ALU端口。 (但只有其中的3个可以处理向量uop,因此未消除的向量移动更容易成为瓶颈,特别是在没有许多负载或存储占据前端带宽(每时钟周期4个融合域uop)的代码中将前端带宽从ALU uop中取走。此外,对执行单元进行uop调度并非完美(更像是最旧准备就绪),因此不在关键路径上的uop可以从关键路径中窃取周期)。

如果我们在循环中加入nop或者xor edx,edx,它们也会被发出但不会在Intel SnB系列CPU上执行。
零延迟的mov消除对于从32位扩展到64位以及从8位扩展到64位非常有用。(movzx eax, bl 被消除了,movzx eax, bx没有)。

没有mov消除

所有支持mov消除的当前CPU都不支持对mov same,same进行消除,因此为32到64位的整数选择不同的寄存器进行零扩展,或者在罕见情况下使用vmovdqa xmm,xmm进行零扩展到YMM。(除非你需要结果在它已经在的寄存器中。来回跳到另一个寄存器并返回通常更糟糕。) 在英特尔上,对于例如movzx eax,al也适用相同的规则。(AMD Ryzen不会移动消除movzx。) Agner Fog的指令表显示mov总是在Ryzen上被消除,但我想他的意思是它不能像在英特尔上那样在两个不同的寄存器之间失败。

我们可以利用这个限制来创建一个故意击败它的微基准。

mov ecx, ecx      # CPUs can't eliminate  mov same,same
lea ecx, [rcx-1]

dec ecx
jnz .loop

 3,000,320,972      cycles:u                  #    3.898 GHz                      ( +-  0.00% )
 4,000,000,238      instructions:u            #    1.33  insn per cycle           ( +-  0.00% )
 1,000,000,234      branches:u                # 1299.225 M/sec                    ( +-  0.00% )
 3,000,084,446      uops_issued_any:u         # 3897.783 M/sec                    ( +-  0.00% )
 3,000,058,661      uops_executed_thread:u    # 3897.750 M/sec                    ( +-  0.00% )

这需要3G个周期来完成1G次迭代,因为依赖链的长度现在是3个周期。
融合域uop计数没有变化,仍为3G。
改变的是未融合域uop计数与融合域相同。所有的uops都需要一个执行单元;没有一个mov指令被消除,所以它们都给循环担负的依赖链增加了1个周期的延迟。
(当有微型融合uops时,比如 add eax,[rsi],uops_executed计数可能会高于uops_issued。但我们现在没有这种情况。)

完全没有mov

lea ecx, [rcx-1]

dec ecx
jnz .loop


 2,000,131,323      cycles:u                  #    3.896 GHz                      ( +-  0.00% )
 3,000,000,161      instructions:u            #    1.50  insn per cycle         
 1,000,000,157      branches:u                # 1947.876 M/sec                  
 2,000,055,428      uops_issued_any:u         # 3895.859 M/sec                    ( +-  0.00% )
 2,000,039,061      uops_executed_thread:u    # 3895.828 M/sec                    ( +-  0.00% )

现在我们的循环依赖链路延迟降至2个周期。

没有任何东西被消除。


我在一台3.9GHz i7-6700k Skylake上进行了测试。对于所有perf事件,我在Haswell i5-4210U上获得了相同的结果(在1G计数中相差不到40k)。这大约是在同一系统上重新运行的误差范围。
请注意,如果我以root身份运行perf,并且计算周期而不是cycles:u(仅用户空间),它会将CPU频率测量为恰好3.900 GHz。(我不知道为什么Linux只在重新启动后遵从max turbo的bios设置,但是如果我让它闲置几分钟,它就会降到3.9 GHz。Asus Z170 Pro Gaming主板,带有内核4.10.11-1-ARCH的Arch Linux。在Ubuntu上也看到了同样的事情。从/etc/rc.local向/sys/devices/system/cpu/cpufreq/policy[0-9]*/energy_performance_preference写入balance_performance可以修复它,但是编写balance_power会使它稍后再次降至3.9GHz。)

1:更新:作为运行sudo perf的更好选择,我在/etc/syctl.d/99-local.conf中设置了sysctl kernel.perf_event_paranoid = 0


你应该在 AMD Ryzen 上获得相同的结果,因为它可以消除整数 mov。AMD Bulldozer 家族只能消除 xmm 寄存器副本。(根据 Agner Fog 的说法,ymm 寄存器副本是消除低半部分和高半部分的 ALU 操作。)
例如,AMD Bulldozer 和 Intel Ivybridge 可以每个时钟周期维持 1 的吞吐量。
 movaps  xmm0, xmm1
 movaps  xmm2, xmm3
 movaps  xmm4, xmm5
 dec
 jnz .loop

但是Intel Sandybridge不能消除操作,因此它会在3个执行端口中的4个ALU uops上出现瓶颈。如果使用pxor xmm0,xmm0而不是movaps,则SnB也可以每个时钟周期维持一个迭代。(但Bulldozer系列不能,因为在AMD上,xor-zeroing仍需要执行单元,即使与寄存器的旧值无关。而且Bulldozer系列的PXOR吞吐量只有0.5c。)

移动指令消除的局限性

连续两个依赖MOV指令会暴露Haswell和Skylake之间的差异。

.loop:
  mov eax, ecx
  mov ecx, eax

  sub ecx, 2
  jnz .loop

Haswell:次要的运行到运行的变化(1.746到1.749 c/iter),但这是典型的:

 1,749,102,925      cycles:u                  #    2.690 GHz                    
 4,000,000,212      instructions:u            #    2.29  insn per cycle         
 1,000,000,208      branches:u                # 1538.062 M/sec                  
 3,000,079,561      uops_issued_any:u         # 4614.308 M/sec                  
 1,746,698,502      uops_executed_core:u      # 2686.531 M/sec                  
   745,676,067      lsd_cycles_4_uops:u       # 1146.896 M/sec                  
  

不是所有的MOV指令都被消除了:每次迭代中约有0.75个2个指令需要执行端口。每个未被消除的MOV指令都会增加1c的延迟到循环传递的依赖链中,因此和非常相似并非巧合。所有的uops都是单一依赖链的一部分,因此没有并行性可言。无论运行到运行的变化如何,始终比高约5M,因此我猜还有5M个周期被用在其他地方。

Skylake:结果比HSW更稳定,并且更多的MOV被消除:每2个指令中只有0.6666个指令需要执行单元。

 1,666,716,605      cycles:u                  #    3.897 GHz
 4,000,000,136      instructions:u            #    2.40  insn per cycle
 1,000,000,132      branches:u                # 2338.050 M/sec
 3,000,059,008      uops_issued_any:u         # 7014.288 M/sec
 1,666,548,206      uops_executed_thread:u    # 3896.473 M/sec
   666,683,358      lsd_cycles_4_uops:u       # 1558.739 M/sec

在Haswell架构中,lsd.cycles_4_uops占据了所有的uops。(0.745 * 4 ~= 3)。因此,在几乎每个发出任何uops的周期中,都会发出一个完整的4组(从循环缓冲区)。我可能应该看一下不关心它们来自哪里的其他计数器,比如uops_issued.stall_cycles来计算没有发出uops的周期。
但是在SKL架构中,0.66666 * 4 = 2.66664小于3,因此在某些周期中,前端发出的uops少于4个。(通常会停顿,直到乱序后端有足够的空间发出完整的4组,而不是发出不完整的组)。
这很奇怪,我不知道确切的微架构限制是什么。由于循环只有3个uops,每个4个uops的发布组都超过了完整迭代。因此,一个发布组可以包含多达3个依赖性MOV。也许Skylake有时设计成打破这一点,以允许更多的mov消除? 更新: 实际上,Skylake上的3-uop循环是正常的。 uops_issued.stall_cycles显示HSW和SKL以与此相同的方式发出简单的3 uop loop,没有mov-elimination。因此,更好的mov-elimination是由于出于某种其他原因而分裂发布组的副作用。(这不是瓶颈,因为无论如何,条件分支无法比每个时钟周期执行的速度更快)。我仍然不知道为什么SKL不同,但我不认为这是什么值得担心的事情。
在一个不那么极端的情况下,SKL和HSW是相同的,两者都无法消除每2个MOV指令中的0.3333:
.loop:
  mov eax, ecx
  dec eax
  mov ecx, eax

  sub ecx, 1
  jnz .loop

 2,333,434,710      cycles:u                  #    3.897 GHz                    
 5,000,000,185      instructions:u            #    2.14  insn per cycle         
 1,000,000,181      branches:u                # 1669.905 M/sec                  
 4,000,061,152      uops_issued_any:u         # 6679.720 M/sec                  
 2,333,374,781      uops_executed_thread:u    # 3896.513 M/sec                  
 1,000,000,942      lsd_cycles_4_uops:u       # 1669.906 M/sec                  

所有的uop都是以4个为一组进行发布。任何连续的4个uop中将包含恰好两个MOV uop,这些uop可能会被消除。既然它在某些周期内成功地消除了两个uop,那我不知道为什么它不能总是这样做。

英特尔优化手册指出,尽早覆盖mov-elimination的结果可以释放微架构资源,使其更容易成功,至少对于movzx是如此。请参见示例3-23。重新排序序列以提高零延迟MOV指令的有效性

因此,也许它在内部通过有限大小的引用计数表进行跟踪?如果它作为mov目标的值仍然需要,那么必须有一些东西阻止物理寄存器文件条目在不再需要作为原始体系结构寄存器的值时被释放。尽早释放PRF条目非常重要,因为PRF大小可能会限制乱序窗口小于ROB大小。

我尝试了Haswell和Skylake上的示例,并发现在这样做时mov-elimination确实更多地起作用,但总周期实际上略慢而不是更快。该示例旨在展示IvyBridge的好处,它可能会在其3个ALU端口上出现瓶颈,但HSW/SKL只会在dep链中的资源冲突上出现瓶颈,并且似乎不需要一个ALU端口来执行更多的movzx指令。
另请参见为什么在现代Intel架构中XCHG reg,reg是3个微操作指令?以获取有关mov-elimination如何工作以及它是否适用于xchg eax,ecx的更多研究和猜测。(实际上,在Intel上,xchg reg,reg是3个ALU uops,但在Ryzen上则是2个消除uops。有趣的是猜测Intel是否可以更有效地实现它。)

顺便提一下,由于Haswell芯片上的一个错误,当启用超线程时,Linux不提供uops_executed.thread,只提供uops_executed.core。另一个内核在整个过程中肯定是空闲的,甚至没有计时器中断 因为我使用echo 0 > /sys/devices/system/cpu/cpu3/online将其离线了。不幸的是,在内核的perf驱动程序(PAPI)决定启用HT之前无法完成此操作,并且我的Dell笔记本电脑没有BIOS选项可以禁用HT。因此,在该系统上我无法让perf同时使用所有8个硬件PMU计数器,而只能使用其中4个 :/。


2
+1 很棒的回答!其中一些内容超出了我的理解范围(例如,我之前从未听说过“融合域”),但我认为我理解了正在发生的事情。谢谢! - user541686
2
是的,我相当确定我理解了。你的意思是dec + jnz会融合成一个操作,所以如果mov被消除,你就有了每4个指令运行2个操作的情况,每个操作需要一个周期,从而得到2.00 ins/cycle,1.33和1.50的情况类似。那个2%确实很奇怪,我同意。但这是一个非常好的答案;我打算在某个时候接受它,只是不着急。谢谢你写下它。 - user541686
2
@JDługosz: movzx eax, bl 是8位到64位的转换。32位->64位的部分是隐含的,因为写入32位寄存器 (https://dev59.com/uWgu5IYBdhLWcg3wpYjI)。写入 movzx rax, bl 会使代码变大(需要REX前缀),但没有任何好处。 - Peter Cordes
2
@BeeOnRope:天啊,Intel,请好好测试你们的CPU,这样我们就不必绕过由缓解措施引入的性能障碍了。特别是因为Intel对IvyBridge的优化建议是尽量立即覆盖mov的结果以释放移动消除资源,从而更有可能使mov在关键路径上无法被消除。(而编译器似乎更喜欢在复制后使用副本而不是原始值进行更多操作。) - Peter Cordes
1
@Noah:很遗憾,英特尔微码不是开源的;我们知道LSD可以通过微码禁用,就像在Skylake系列中一样。(当然,如果你有多台电脑可供选择,你可以使用一个通过微码禁用了LSD的SKL,而不是一个没有禁用的,假设它们在其他微架构方面是相同的。) - Peter Cordes
显示剩余12条评论

12

这里有两个小测试,我相信它们可以明确地证明mov-elimination的证据:

__loop1:
    add edx, 1
    add edx, 1
    add ecx, 1
    jnc __loop1

对比

__loop2:
    mov eax, edx
    add eax, 1
    mov edx, eax
    add edx, 1
    add ecx, 1
    jnc __loop2
如果mov添加了一个周期到依赖链中,那么第二个版本每次迭代大约需要4个周期。在我的Haswell上,两者每次迭代都需要大约2个周期,这是不可能的,没有mov消除。

4
由于现在这些“mov”指令处于依赖链中,所以如果它们有延迟,延迟时间会相互累加。在你的测试用例中,“mov”指令只是在链的末尾悬空,没有任何等待它执行的指令。它可能被消除,也可能不被消除,无法确定。 - harold
3
“@Mehrdad,时序是不同的。但延迟只能是整数个时钟周期,除非像Netburst那样带有奇怪的双泵ALU。所以 mov 指令要么增加一个时钟周期,要么就没有(这种情况下它必须被消除了)。其存在会带来其他(更微妙的)影响,这实际上是无关的。当然,您绝对正确,这些影响确实存在。” - harold
1
@Mehrdad 这有点进入奇怪的情况,因为它取决于如何实现,至少可以尝试测量它,因为它概念上读取某些内容并写入某些内容。实际执行(例如通过调整我的第二个测试用例中的代码)显示其延迟在Haswell上为1(即未被消除)。我想不出原因,但就是这样。 - harold
2
@Mehrdad 对不起,是的,平均延迟可以是非整数。在假设发生的情况是偶尔未能消除mov的情况下,您甚至可以说延迟平均为一些低但非零的数字。据我所知,这仅是由于其他效应造成的,但值得一试。例如,如果第二个示例的一致小惩罚在将“其他无害垃圾”放入其中而不是mov时发生显着变化,则可能表明在该方向上有趣的东西。 - harold
1
你是在裸机上运行吗?启用或禁用缓存?你通过至少16个字节,如果不是32个字节来调整提取对齐方式? - old_timer
显示剩余12条评论

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