为什么mov啊,bh和mov al,bl比单指令mov ax,bx快得多?

时间:2011-08-11 19:27:25

标签: x86 assembly

我发现了

mov al, bl
mov ah, bh

快得多
mov ax, bx

任何人都可以解释为什么吗? 我在Windows XP下以32位模式运行Core 2 Duo 3 Ghz。 使用NASM进行编译,然后与VS2010进行链接。 Nasm编译命令:

nasm -f coff -o triangle.o triangle.asm

这是我用来渲染三角形的主循环:

; some variables on stack
%define cr  DWORD [ebp-20]
%define dcr DWORD [ebp-24]
%define dcg DWORD [ebp-32]
%define dcb DWORD [ebp-40]

loop:

add esi, dcg
mov eax, esi
shr eax, 8

add edi, dcb
mov ebx, edi
shr ebx, 16
mov bh, ah

mov eax, cr
add eax, dcr
mov cr, eax

mov ah, bh  ; faster
mov al, bl
;mov ax, bx

mov DWORD [edx], eax

add edx, 4

dec ecx
jge loop

我可以为整个VS项目提供测试源。

4 个答案:

答案 0 :(得分:10)

为什么慢?
使用16位寄存器的原因是昂贵的,而使用8位寄存器则是在微码中解码16位寄存器指令。这意味着在解码期间需要额外的周期,并且在解码时无法配对 另外因为ax是一个部分寄存器,所以需要一个额外的周期来执行,因为寄存器的顶部需要与写入下部相结合。
8位写入具有特殊硬件以加快速度,但16位写入则没有。同样在许多处理器上,16位指令需要2个周期而不是1个,并且它们不允许配对。

这意味着,不是能够在4个周期内处理12个指令(每个周期3个),现在只能执行1,因为在将指令解码为微码时会出现停顿,在处理微码时会出现停顿。 / p>

如何让它更快?

mov al, bl
mov ah, bh

(此代码至少需要2个CPU周期,并且可能会在第二条指令上停顿,因为在某些(较旧的)x86 CPU上,您可以锁定EAX)
这是发生的事情:

  • 读取EAX。 (第1周期)
    • EAX的低位字节已更改(仍为周期1)
    • 并将完整值写回EAX。 (第1周期)
  • EAX被锁定以进行写入,直到第一次写入完全解析为止。 (潜在等待多个周期)
  • 对EAX中的高字节重复该过程。 (第2周期)

在最新的Core2 CPU上,这不是一个问题,因为已经建立了额外的硬件,知道blbh真的永远不会相互接触。

mov eax, ebx

一次移动4个字节,该单个指令将以1个cpu周期运行(并且可以与其他并行指令配对)。

  • 如果您想要快速代码,请始终使用32位(EAX,EBX等)寄存器。
  • 除非必须,否则尽量避免使用8位子寄存器。
  • 切勿使用16位寄存器。即使你必须在32位模式下使用5条指令,它仍然会更快。
  • 使用movzx reg,...(或movsx reg,...)说明

加快代码
我看到一些加速代码的机会。

; some variables on stack
%define cr  DWORD [ebp-20]
%define dcr DWORD [ebp-24]
%define dcg DWORD [ebp-32]
%define dcb DWORD [ebp-40]

mov edx,cr

loop:

add esi, dcg
mov eax, esi
shr eax, 8

add edi, dcb
mov ebx, edi
shr ebx, 16   ;higher 16 bits in ebx will be empty.
mov bh, ah

;mov eax, cr   
;add eax, dcr
;mov cr, eax

add edx,dcr
mov eax,edx

and eax,0xFFFF0000  ; clear lower 16 bits in EAX
or eax,ebx          ; merge the two. 
;mov ah, bh  ; faster
;mov al, bl


mov DWORD [epb+offset+ecx*4], eax ; requires storing the data in reverse order. 
;add edx, 4

sub ecx,1  ;dec ecx does not change the carry flag, which can cause
           ;a false dependency on previous instructions which do change CF    
jge loop

答案 1 :(得分:8)

我的Core 2 Duo CPU L9300 1.60GHz也更快。正如我在评论中所写,我认为这与使用部分寄存器(ahalax)有关。查看更多信息hereherehere(第88页)。

我已经编写了一个小测试套件来尝试改进代码,虽然不使用OP中提供的ax版本是最聪明的,但尝试消除部分寄存器使用确实提高了速度(甚至比我快速尝试释放另一个寄存器更多。)

为了获得有关为什么一个版本比另一个版本更快的原因的更多信息,我认为需要更仔细地阅读源材料和/或使用诸如英特尔VTune或AMD CodeAnalyst之类的东西。 (可能会发现我错了)

UPDATE,虽然下面的oprofile输出没有证明什么,它确实表明在两个版本中都发生了很多部分寄存器停顿,但是在最慢的版本(triAsm2)中大约是'fast'的两倍'版本(triAsm1)。

$ opreport -l test                            
CPU: Core 2, speed 1600 MHz (estimated)
Counted CPU_CLK_UNHALTED events (Clock cycles when not halted) with a unit mask of 0x00 (Unhalted core cycles) count 800500
Counted RAT_STALLS events (Partial register stall cycles) with a unit mask of 0x0f (All RAT) count 1000000
samples  %        samples  %        symbol name
21039    27.3767  10627    52.3885  triAsm2.loop
16125    20.9824  4815     23.7368  triC
14439    18.7885  4828     23.8008  triAsm1.loop
12557    16.3396  0              0  triAsm3.loop
12161    15.8243  8         0.0394  triAsm4.loop

Complete oprofile output

结果:

triC:7410.000000 ms,a5afb9(asm代码的C实现)

triAsm1:6690.000000 ms,a5afb9(来自OP的代码,使用alah

triAsm2:9290.000000 ms,a5afb9(来自OP的代码,使用ax

triAsm3:5760.000000 ms,a5afb9(将OP代码直接转换为没有部分寄存器使用的代码)

triAsm4:5640.000000毫秒,a5afb9(快速尝试加快速度)

这是我的测试套件,使用-std=c99 -ggdb -m32 -O3 -march=native -mtune=native编译:

test.c的:

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <time.h>

extern void triC(uint32_t* dest, uint32_t cnt, uint32_t cr, uint32_t cg, uint32_t cb, uint32_t dcr, uint32_t dcg, uint32_t dcb);
extern void triAsm1(uint32_t* dest, uint32_t cnt, uint32_t cr, uint32_t cg, uint32_t cb, uint32_t dcr, uint32_t dcg, uint32_t dcb);
extern void triAsm2(uint32_t* dest, uint32_t cnt, uint32_t cr, uint32_t cg, uint32_t cb, uint32_t dcr, uint32_t dcg, uint32_t dcb);
extern void triAsm3(uint32_t* dest, uint32_t cnt, uint32_t cr, uint32_t cg, uint32_t cb, uint32_t dcr, uint32_t dcg, uint32_t dcb);
extern void triAsm4(uint32_t* dest, uint32_t cnt, uint32_t cr, uint32_t cg, uint32_t cb, uint32_t dcr, uint32_t dcg, uint32_t dcb);

uint32_t scanline[640];

#define test(tri) \
    {\
        clock_t start = clock();\
        srand(60);\
        for (int i = 0; i < 5000000; i++) {\
            tri(scanline, rand() % 640, 10<<16, 20<<16, 30<<16, 1<<14, 1<<14, 1<<14);\
        }\
        printf(#tri ": %f ms, %x\n",(clock()-start)*1000.0/CLOCKS_PER_SEC,scanline[620]);\
    }

int main() {
    test(triC);
    test(triAsm1);
    test(triAsm2);
    test(triAsm3);
    test(triAsm4);
    return 0;
}

tri.c:

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>

void triC(uint32_t* dest, uint32_t cnt, uint32_t cr, uint32_t cg, uint32_t cb, uint32_t dcr, uint32_t dcg, uint32_t dcb) {
    while (cnt--) {
        cr += dcr;
        cg += dcg;
        cb += dcb;
        *dest++ = (cr & 0xffff0000) | ((cg >> 8) & 0xff00) | ((cb >> 16) & 0xff);
    }
}

atri.asm:

    bits 32
    section .text
    global triAsm1
    global triAsm2
    global triAsm3
    global triAsm4

%define cr DWORD [ebp+0x10]
%define dcr DWORD [ebp+0x1c]
%define dcg DWORD [ebp+0x20]
%define dcb DWORD [ebp+0x24]

triAsm1:
    push ebp
    mov ebp, esp

    pusha

    mov edx, [ebp+0x08] ; dest
    mov ecx, [ebp+0x0c] ; cnt
    mov esi, [ebp+0x14] ; cg
    mov edi, [ebp+0x18] ; cb

.loop:

    add esi, dcg
    mov eax, esi
    shr eax, 8

    add edi, dcb
    mov ebx, edi
    shr ebx, 16
    mov bh, ah

    mov eax, cr
    add eax, dcr
    mov cr, eax

    mov ah, bh  ; faster
    mov al, bl

    mov DWORD [edx], eax

    add edx, 4

    dec ecx
    jge .loop

    popa

    pop ebp
    ret


triAsm2:
    push ebp
    mov ebp, esp

    pusha

    mov edx, [ebp+0x08] ; dest
    mov ecx, [ebp+0x0c] ; cnt
    mov esi, [ebp+0x14] ; cg
    mov edi, [ebp+0x18] ; cb

.loop:

    add esi, dcg
    mov eax, esi
    shr eax, 8

    add edi, dcb
    mov ebx, edi
    shr ebx, 16
    mov bh, ah

    mov eax, cr
    add eax, dcr
    mov cr, eax

    mov ax, bx ; slower

    mov DWORD [edx], eax

    add edx, 4

    dec ecx
    jge .loop

    popa

    pop ebp
    ret

triAsm3:
    push ebp
    mov ebp, esp

    pusha

    mov edx, [ebp+0x08] ; dest
    mov ecx, [ebp+0x0c] ; cnt
    mov esi, [ebp+0x14] ; cg
    mov edi, [ebp+0x18] ; cb

.loop:
    mov eax, cr
    add eax, dcr
    mov cr, eax

    and eax, 0xffff0000

    add esi, dcg
    mov ebx, esi
    shr ebx, 8
    and ebx, 0x0000ff00
    or eax, ebx

    add edi, dcb
    mov ebx, edi
    shr ebx, 16
    and ebx, 0x000000ff
    or eax, ebx

    mov DWORD [edx], eax

    add edx, 4

    dec ecx
    jge .loop

    popa

    pop ebp
    ret

triAsm4:
    push ebp
    mov ebp, esp

    pusha

    mov [stackptr], esp

    mov edi, [ebp+0x08] ; dest
    mov ecx, [ebp+0x0c] ; cnt
    mov edx, [ebp+0x10] ; cr
    mov esi, [ebp+0x14] ; cg
    mov esp, [ebp+0x18] ; cb

.loop:
    add edx, dcr
    add esi, dcg
    add esp, dcb

    ;*dest++ = (cr & 0xffff0000) | ((cg >> 8) & 0xff00) | ((cb >> 16) & 0xff);
    mov eax, edx ; eax=cr
    and eax, 0xffff0000

    mov ebx, esi ; ebx=cg
    shr ebx, 8
    and ebx, 0xff00
    or eax, ebx
    ;mov ah, bh

    mov ebx, esp
    shr ebx, 16
    and ebx, 0xff
    or eax, ebx
    ;mov al, bl

    mov DWORD [edi], eax
    add edi, 4

    dec ecx
    jge .loop

    mov esp, [stackptr]

    popa

    pop ebp
    ret

    section .data
stackptr: dd 0

答案 2 :(得分:6)

摘要:16位指令不是直接问题。 问题是在写入部分寄存器后读取更宽的寄存器,导致Core2上出现部分寄存器停顿。这在Sandybridge以及后来的问题上要小得多,因为它们合并得更便宜。 mov ax, bx导致额外的合并,但即便是OP&#34;快速&#34;版本有一些摊位。

请参阅本答案的结尾,以获得备用标量内循环,它应该比其他两个答案更快,使用shld在寄存器之间混洗字节。在循环外部将8b左移的东西放在每个寄存器的顶部,我们想要的字节,这使得这非常便宜。它应该在32位core2上以每4个时钟周期的一次迭代运行稍微好一点,并且在没有停顿的情况下使所有三个执行端口饱和。它应该在Haswell上每2.5c运行一次。

要实际快速执行此操作,请查看auto-vectorized compiler output,然后使用向量内在函数减少或重新实现。

与16位操作数大小指令缓慢的说法相反,Core2理论上可以在每个时钟交替mov ax, bxmov ecx, edx维持3个insn。没有&#34;模式开关&#34;任何形式的。 (正如大家所指出的那样,&#34;上下文切换&#34;是一个可怕的名称选择,因为它已经具有特定的技术含义。)

当您阅读之前仅编写部分内容的reg时,问题是部分寄存器停止。英特尔P6系列CPU不是强制写入ax等待eax的旧内容准备就绪(错误依赖),而是分别跟踪部分注册表的依赖关系。阅读更广泛的reg强制合并,根据Agner Fog停止2到3个周期。使用16位操作数大小的另一个大问题是立即操作数,你可以在英特尔CPU上的解码器中获得LCP停顿,以获得不适合imm8的即时操作。

SnB-family效率更高,只需插入一个额外的uop来进行合并,而不会停顿。 AMD和英特尔Silvermont(和P4)根本没有单独重命名部分寄存器,所以他们确实有&#34; false&#34;依赖于以前的内容。在这种情况下,我们稍后会读取完整的寄存器,因此它是真正的依赖,因为我们想要合并,因此这些CPU具有优势。 (英特尔Haswell / Skylake(也许是IvB)不会将AL重新命名为RAX;它们只是分别重命名AH / BH / CH / DH。读取high8寄存器会产生额外的延迟。  见this Q&A about partial registers on HSW/SKL for the details。)

由于合并的reg在下一次迭代中被覆盖,因此部分reg停顿都不是长依赖链的一部分。显然Core2只是拖延了前端,甚至是整个乱序执行核心?我想问一个关于Core2上部分寄存器减速有多昂贵的问题,以及如何衡量SnB的成本。 @ user786653的oprofile回答揭示了它的一些亮点。 (还有一些非常有用的C反向工程,来自OP的asm,以帮助明确这个功能真正想要实现的目标)。

使用现代gcc编译C可以生成向量化的asm,它在xmm寄存器中一次执行4个dword循环。但是,它可以使用SSE4.1做得更好。 (并且clang并没有使用-march=core2自动向量化它,但它确实展开很多次,可能会交错多次迭代以避免部分注册的东西。)如果你不告诉gcc那么dest已对齐,它会在矢量化循环周围生成大量的标量序言/结尾,以达到对齐的点。

它将整数args转换为向量常量(在堆栈上,因为32位代码只有8个向量寄存器)。 The inner loop is

.L4:
        movdqa  xmm0, XMMWORD PTR [esp+64]
        mov     ecx, edx
        add     edx, 1
        sal     ecx, 4
        paddd   xmm0, xmm3
        paddd   xmm3, XMMWORD PTR [esp+16]
        psrld   xmm0, 8
        movdqa  xmm1, xmm0
        movdqa  xmm0, XMMWORD PTR [esp+80]
        pand    xmm1, xmm7
        paddd   xmm0, xmm2
        paddd   xmm2, XMMWORD PTR [esp+32]
        psrld   xmm0, 16
        pand    xmm0, xmm6
        por     xmm0, xmm1
        movdqa  xmm1, XMMWORD PTR [esp+48]
        paddd   xmm1, xmm4
        paddd   xmm4, XMMWORD PTR [esp]
        pand    xmm1, xmm5
        por     xmm0, xmm1
        movaps  XMMWORD PTR [eax+ecx], xmm0
        cmp     ebp, edx
        ja      .L4

请注意,整个循环中有一个商店。所有载荷都只是之前计算的向量,作为本地存储在堆栈中。

有几种方法可以加快OP的代码。最明显的是,我们不需要制作堆栈框架,从而释放ebp。最明显的用途是保持cr,OP溢出到堆栈。 user786653的triAsm4执行此操作,除了他使用疯狂的巨魔逻辑变体:他创建一个堆栈帧并按常规设置ebp,但随后会隐藏esp静态位置并将其用作临时寄存器!!如果你的程序有任何信号处理程序,这显然会严重破坏,但是否则很好(除了使调试更难)。

如果您想要使用esp作为划痕,请将函数args复制到静态位置,这样您就不需要注册来保存任何内容指向堆栈内存的指针。 (将旧的esp保存在MMX寄存器中也是一种选择,因此您可以在一次使用多个线程的可重入函数中执行此操作。但是如果您将args复制到某个静态的地方,则不会这样做,除非它是&#39;使用段覆盖或其他东西进行线程局部存储。你不必担心同一个线程内的重入,因为堆栈指针处于不可用状态。任何类似信号处理程序的东西都可以重新启动 - 在同一个线程中输入你的函数会反而崩溃。&gt;。&lt;)

溢出cr实际上不是最佳选择:我们可以将dst指针保存在寄存器中,而不是使用两个寄存器进行循环(计数器和指针)。通过计算结束指针(一个超过结尾:dst+4*cnt)来执行循环边界,并使用带有内存操作数的cmp作为循环条件。

cmp / jb的结束指针进行比较实际上在Core2上比dec / jge更优化。无符号条件可以与cmp进行宏融合。在SnB之前,只有cmptest可以进行宏观融合。 (对于AMD Bulldozer也是如此,但cmp和测试可以与AMD上的任何jcc融合)。 SnB系列CPU可以对dec / jge进行宏融合。有趣的是,Core2只能将已签名的比较(如jge)与test进行宏融合,而非cmp。 (无符号比较无论如何都是地址的正确选择,因为0x8000000并不特殊,但0是。我没有使用jb作为风险优化。)

我们无法将cbdcb预先移位到低字节,因为它们需要在内部保持更高的精度。但是,我们可以移动另外两个,因此它们会反对其寄存器的左边缘。将它们向右移动到目标位置不会使可能溢出的任何垃圾高位保留。

我们可以做重叠商店,而不是合并到eax。从eax存储4B,然后从bx存储低2B。这样可以节省eax中的部分注册失速,但会产生一个用于将bh合并到ebx中,因此价值有限。可能4B写和两个重叠的1B商店实际上在这里很好,但这开始是很多商店。尽管如此,它可能会分散在足够的其他指令上,而不会在商店端口上出现瓶颈。

user786653的triAsm3使用屏蔽和or指令进行合并,这看起来像是Core2的合理方法。对于AMD,Silvermont或P4,使用8b和16b mov指令合并部分寄存器实际上可能是好的。如果您只编写low8或low16以避免合并处罚,您也可以在Ivybridge / Haswell / Skylake上利用它。但是,我提出了多项改进,需要更少的屏蔽。

; use defines you can put [] around so it's clear they're memory refs
; %define cr  ebp+0x10
%define cr  esp+something that depends on how much we pushed
%define dcr ebp+0x1c  ;; change these to work from ebp, too.
%define dcg ebp+0x20
%define dcb ebp+0x24

; esp-relative offsets may be wrong, just quickly did it in my head without testing:
; we push 3 more regs after ebp, which was the point at which ebp snapshots esp in the stack-frame version.  So add 0xc (i.e. mentally add 0x10 and subract 4)
; 32bit code is dumb anyway.  64bit passes args in regs.

%define dest_arg  esp+14
%define cnt_arg   esp+18
... everything else

tri_pjc:
    push    ebp
    push    edi
    push    esi
    push    ebx  ; only these 4 need to be preserved in the normal 32bit calling convention

    mov     ebp, [cr]
    mov     esi, [cg]
    mov     edi, [cb]

    shl     esi,   8          ; put the bits we want at the high edge, so we don't have to mask after shifting in zeros
    shl     [dcg], 8
    shl     edi,   8
    shl     [dcb], 8
       ; apparently the original code doesn't care if cr overflows into the top byte.

    mov     edx, [dest_arg]
    mov     ecx, [cnt_arg]
    lea     ecx, [edx + ecx*4] ; one-past the end, to be used as a loop boundary
    mov    [dest_arg], ecx    ; spill it back to the stack, where we only need to read it.

ALIGN 16
.loop: ; SEE BELOW, this inner loop can be even more optimized
    add     esi, [dcg]
    mov     eax, esi
    shr     eax, 24           ; eax bytes = { 0  0  0 cg }

    add     edi, [dcb]
    shld    eax, edi, 8       ; eax bytes = { 0  0 cg cb }

    add     ebp, [dcr]
    mov     ecx, ebp
    and     ecx, 0xffff0000
    or      eax, ecx          ; eax bytes = { x cr cg cb}  where x is overflow from cr.  Kill that by changing the mask to 0x00ff0000
    ; another shld to merge might be faster on other CPUs, but not core2
    ; merging with mov cx, ax   would also be possible on CPUs where that's cheap (AMD, and Intel IvB and later)

    mov    DWORD [edx], eax
    ; alternatively:
    ; mov    DWORD [edx], ebp
    ; mov     WORD [edx], eax   ; this insn replaces the mov/and/or merging

    add     edx, 4
    cmp     edx, [dest_arg]   ; core2 can macro-fuse cmp/unsigned condition, but not signed
    jb .loop

    pop     ebx
    pop     esi
    pop     edi
    pop     ebp
    ret

在完成omit-frame-pointer并将循环边界放入内存后,我最终得到的寄存器多于我需要的寄存器。您可以在寄存器中缓存一些额外的东西,或者避免保存/恢复寄存器。也许保持ebx中的循环边界是最好的选择。它基本上保存了一个序言指令。将dcbdcg保留在寄存器中需要在序言中添加额外的insn才能加载它。 (具有内存目标的转移是丑陋和缓慢的,即使在Skylake上,但是代码大小很小。它们不在循环中,而core2没有uop缓存。分别加载/转移/存储是还有3个uops,所以你不能打败它,除非你要把它保存在reg而不是存储。)

shld是P6(Core2)上的2-uop insn。幸运的是,订购循环很容易,因此它是第五条指令,前面是四条单指令。它应该将解码器作为第二组4中的第一个uop命中,因此它不会导致前端延迟。 (Core2 can decode 1-1-1-1,2-1-1-1,3-1-1-1或4-1-1-1 uops-per-insn模式.SnB及以后重新设计了解码器,并添加了一个uop缓存,使解码通常不是瓶颈,并且只能处理1-1-1-1,2-1-1,3-1和4的组。)

shldhorrible on AMD K8, K10, Bulldozer-family, and Jaguar。 6 m-ops,3c延迟,每3c吞吐量一个。 Atom / Silvermont在32位操作数上非常棒,但16或64b寄存器很糟糕。

这个insn排序可能会将cmp解码为组的最后一个insn,然后单独jb解码,使其不是宏保险丝。如果前端效果是此循环的一个因素,这可能会给重叠存储合并方法带来额外的好处,而不仅仅是保存uop。 (我怀疑它们会是高度并行性并且循环携带的dep链很短,所以可以同时进行多次迭代的工作。)

所以:每次迭代的融合域uops:Core2上的13(假设可能实际上不会发生宏融合),在SnB家族上为12。所以IvB应该在每3c一次迭代运行它(假设3个ALU端口都不是瓶颈。mov r,r不需要ALU端口,商店也没有。add和布尔值可以使用任何端口。shrshld是唯一不能在多种端口上运行的端口,并且每三个周期只有两个移位。)Core2每次迭代需要4c发布它即使它设法避免任何前端瓶颈,甚至更长时间运行它。

我们可能仍然在Core2上运行得足够快,如果我们仍然这样做,那么每次迭代都会将cr溢出/重新加载到堆栈会成为瓶颈。它为循环携带的依赖链添加了一个内存往返(5c),使得总的dep链长度为6个周期(包括add)。

嗯,实际上即使Core2可能会因使用两个shld insn来合并而获胜。它还保存了另一个寄存器!

ALIGN 16
;mov ebx, 111           ; IACA start
;db 0x64, 0x67, 0x90
.loop:
    add     ebp, [dcr]
    mov     eax, ebp
    shr     eax, 16           ; eax bytes = { 0  0  x cr}  where x is overflow from cr.  Kill that pre-shifting cr and dcr like the others, and use shr 24 here

    add     esi, [dcg]
    shld    eax, esi, 8       ; eax bytes = { 0  x cr cg}
    add     edx, 4     ; this goes between the `shld`s to help with decoder throughput on pre-SnB, and to not break macro-fusion.
    add     edi, [dcb]
    shld    eax, edi, 8       ; eax bytes = { x cr cg cb}
    mov    DWORD [edx-4], eax

    cmp     edx, ebx      ; use our spare register here
    jb .loop     ; core2 can macro-fuse cmp/unsigned condition, but not signed.  Macro-fusion works in 32-bit mode only on Core2.

;mov ebx, 222           ; IACA end
;db 0x64, 0x67, 0x90

Per-iteration:SnB:10个融合域uops。 Core2:12个融合域uops,因此 比英特尔CPU上的先前版本短(但在AMD上可怕)。使用shld保存mov指令,因为我们可以使用它来非破坏性地提取源的高字节。

Core2可以每3个时钟一次迭代发出循环。 (这是英特尔的第一个带有4 uop宽管道的CPU)。

Agner Fog's table来自Merom/Conroe (first gen Core2)(请注意,David Kanter的方框图中p2和p5相反):

  • shr:在p0 / p5上运行
  • shld:p0 / p1 / p5为2 uops? Agner的前Haswell表并没有说哪些uops可以去哪里。
  • mov r,raddand:p0 / p1 / p5
  • 融合的cmp-and-branch:p5
  • 存储:p3和p4(这些微融合成1个融合域存储uop)
  • 每次加载:p2。 (所有负载都与融合域中的ALU操作微融合)。

根据具有Nehalem模式而非Core2模式的IACA,大多数shld uop转到p1,其他端口上运行的每个insn平均只有不到0.6。 Nehalem与Core2具有基本相同的执行单元。此处涉及的所有说明对NHM和Core2具有相同的uop成本和端口要求。 IACA的分析对我来说很好,而且我不想自己检查一下这个5岁问题的答案。不过,回答很有趣。 :)

无论如何,根据IACA,uops应该在端口之间很好地分配。它认为Nehalem可以每3.7个周期在一次迭代中运行循环,使所有三个执行端口饱和。它的分析对我来说很好。 (请注意,我必须从cmp中删除内存操作数,以使IACA不会给出愚蠢的结果。)无论如何,这显然是必要的,因为pre-SnB每个周期只能执行一次加载:我们&#39 ;在循环中有四个负载的端口2上的瓶颈。

IACA并不同意Agner Fog对IvB和SnB的测试(根据我在SnB上的测试,它认为shld仍然是2 uops,当它实际上是一个)。所以它的数字很傻。

IACA对于Haswell看起来是正确的,它说瓶颈是前端。它认为HSW可以每2.5c运行一次。 (Haswell中的循环缓冲区至少可以在每次迭代中以非整数个周期发出循环。Sandybridge may be limited to whole numbers of cycles, where the taken loop-branch ends an issue-group。)

我还发现我需要使用iaca.sh -no_interiteration,否则它会认为存在一个带有循环的循环依赖,并认为循环在NHM上需要12c。

答案 3 :(得分:3)

在32位代码中,mov ax, bx需要操作数大小的前缀,而字节大小的移动则不需要。显然,现代处理器设计人员并没有花费太多精力来获得操作数大小的前缀来快速解码,尽管令我惊讶的是,惩罚足以完成两个字节大小的移动。