为什么mov ah,bh和mov al,bl在一起比单指令mov ax,bx快得多?

我发现了

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项目提供测试资源。


为什么它慢
与使用8位寄存器相比,使用16位寄存器的原因很昂贵,即16位寄存器指令在微码中被解码。 这意味着在解码期间额外的周期和解码时无法配对。
另外,因为ax是一个部分寄存器,所以需要一个额外的周期才能执行,因为寄存器的顶部需要与写入下部的组合。
8位写入有特殊的硬件来加速这一点,但16位写入不会。 再次在许多处理器上,16位指令需要2个周期而不是1个周期,并且它们不允许配对。

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

我怎样才能让它更快?

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
    

    我的Core 2 Duo CPU L9300 1.60GHz速度也更快。 正如我在评论中写的,我认为这与使用部分寄存器( ahalax )有关。 看到更多,例如这里,这里和这里(第88页)。

    我已经写了一个小测试套件来尝试和改进代码,而不使用OP中提供的ax版本是最聪明的,试图消除部分寄存器使用在速度上会提高(甚至比我的快速尝试在释放另一个登记册)。

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

    UPDATE,虽然oprofile的下面的输出没有证明任何东西,但它证明在两个版本中都有很多的部分寄存器停顿,但是在最慢版本(triAsm2)中的速度大约是“快速”版本的两倍( 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
    

    完整的oprofile输出。

    结果:

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

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

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

    triAsm3:5760.000000毫秒,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, %xn",(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
    

    总结 :16位指令不是直接的问题。 问题是在写入部分寄存器后读取更宽的寄存器,导致 Core2上的部分寄存器停顿 。 这对于Sandybridge和后来来说更是一个问题,因为它们合并得更便宜。 mov ax, bx会导致额外的合并,但即使是OP的“快速”版本也有一些摊位。

    看到这个答案的结尾是另一个标量内循环,应该比其他两个答案快,使用shld来在寄存器之间混洗字节。 在循环外预留8b剩下的东西将我们想要的字节放在每个寄存器的顶部,这使得它非常便宜。 它的运行速度应该比32位core2每4个时钟周期的迭代次数稍微好一点,并使所有三个执行端口无阻塞。 它应该在Haswell每2.5c一次迭代运行。

    要真正做到这一点,请查看自动矢量化的编译器输出,也许可以减少或重新实现矢量内在函数。


    与16位操作数大小指令缓慢的说法相反,Core2在理论上每个时钟可以交替3个insn,交替mov ax, bxmov ecx, edx 。 没有任何种类的“模式切换”。 (正如所有人都指出的那样,“上下文切换”是构成名称的可怕选择,因为它已具有特定的技术含义。)

    问题是当你阅读一个你以前只写了一部分的注册表时,部分注册失败。 而不是强迫写于ax上的旧内容等待eax准备好(假的依赖),英特尔P6系列CPU的跟踪依赖于单独的部分暂存器。 根据Agner Fog的说法,阅读更广泛的reg部队会合并,这个合并会停止2至3个周期。 使用16位操作数大小的另一个大问题是立即操作数,您可以从LCP在英特尔CPU上的解码器中停止不适用imm8的立即数。

    SnB系列效率更高,只需插入一个额外的uop即可完成合并,而不会拖延。 AMD和英特尔Silvermont(和P4)根本不重新命名部分寄存器,因此它们对前一内容确实具有“错误”的依赖关系。 在这种情况下,我们后来阅读完整寄存器,所以它是一个真正的依赖关系,因为我们想要合并,所以这些CPU有优势。 (Intel Haswell / Skylake(也可能是IvB)不会将AL与ALX分别重命名;它们只能分别重命名AH / BH / CH / DH,而读取high8寄存器会有额外的延迟。请参阅关于HSW / SKL上的部分寄存器的Q&A细节。)


    由于合并后的reg在下一次迭代中被覆盖,所以部分reg条件都不是长依赖链的一部分。 显然,Core2只是拖延了前端,甚至是整个无序执行核心? 我的意思是问一个关于Core2上多少昂贵的部分寄存器放缓以及如何测量SnB上的成本的问题。 @ user786653的oprofile回答对此有所了解。 (并且还有一些真正有用的C从OP的asm反向工程,以帮助明确这个功能真正实现的功能)。

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

    它将整数参数转换为向量常量(在堆栈上,因为32位代码只有8个向量寄存器)。 内部循环是

    .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作为scratch,将函数args复制到静态位置,所以你不需要一个寄存器来保存任何指向堆栈内存的指针。 (将旧的esp保存在MMX寄存器中也是一种选择,因此您可以在多个线程中一次使用的重入函数中执行此操作,但是如果您将args复制到某个静态的地方,则不能这样做,除非它是用线程本地存储你可以不用担心在同一个线程中重新进入,因为堆栈指针处于不可用状态,任何类似于信号处理程序的信号处理程序都可能会在同一个线程中重新进入你的函数崩溃。>。<)

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

    无论如何,与cmp / jb的结束指针相比,在Core2上实际上比dec / jge更优化。 未签名的条件可以与cmp进行宏观融合。 直到SnB,只有cmptest可以完全宏观融合。 (AMD推土机也是如此,但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指令合并部分寄存器可能实际上是好的。 如果您只写低8或低16来避免合并处罚,您也可以在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可以解码1-1-1-1,2-1-1-1,3-1-1-1或4-1-1-1 uops-ins-insn模式,SnB和后来重新设计了解码器,并添加了一个uop缓存,使解码通常不是瓶颈,并且只能处理1-1-1-1,2-1-1,3-1和4组。)

    在AMD K8,K10,推土机系列和捷豹方面, shld是可怕的。 6个操作系统,3个等待时间,以及每3c个吞吐量一个。 在32位操作数大小的Atom / Silvermont上非常棒,但在16位或64位寄存器中可怕。

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

    因此:每次迭代的融合域uop:Core2上的13个(假设宏观融合可能实际上不会发生),12个在SnB家族上。 所以IvB应该每3c一次迭代运行一次(假设3个ALU端口都不是瓶颈, mov r,r不需要ALU端口,store和add都不能使用任何端口, shrshld是唯一无法在多种端口上运行的选项,并且每三个周期只有两次转换)。即使Core2设法避免任何前端瓶颈,甚至更长时间运行。

    我们也许还在酷睿运行速度足够快地洒落/重装cr堆栈每次迭代将是一个瓶颈,如果我们仍然这样做。 它向循环携带的依赖链添加一个内存往返(5c),使得dep链的总长度为6个循环(包括add)。


    嗯,实际上即使是Core2也可能因使用两个shld insns而合并。 它也保存了另一个注册表!

    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
    

    每次迭代:SnB:10个融合域微分。 Core2:12个融合域uops,所以这比Intel CPU上的以前版本更短(但对AMD来说可怕)。 使用shld保存mov指令,因为我们可以使用它来非破坏性地提取源的高字节。

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

    从Agner Fog的Merom / Conroe表格(第一代Core2)(请注意,David Kanter的程序框图将p2和p5颠倒过来):

  • shr :在p0 / p5上运行
  • shld :2 shld为p0 / p1 / p5? Agner的前Haswell表没有说哪个微软可以去哪里。
  • mov r,radd and :p0 / p1 / p5
  • 融合cmp-and-branch:p5
  • 存储:p3和p4(这些微型熔断器分为1个融合域存储器)
  • 每个负载:p2。 (所有负载均与熔融域中的ALU操作进行微熔合)。
  • 根据IACA,它有一个Nehalem模式,但不是Core2,大多数的shld p1,平均每个insn运行在其他端口上的平均值只有不到0.6。 Nehalem与Core2具有基本相同的执行单元。 这里涉及的所有指令对NHM和Core2具有相同的uop成本和端口要求。 IACA的分析对我来说看起来不错,我不想单独检查所有的问题,以回答这个5年前的问题。 虽然这很有趣。 :)

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

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

    IACA看起来对Haswell来说是正确的,它说瓶颈在前端。 它认为HSW可以每2.5c运行一次。 (Haswell中的循环缓冲区至少可以在每次迭代中以非整数个周期发布循环,Sandybridge可能被限制为整个循环数,其中循环分支结束问题组。

    我还发现我需要使用iaca.sh -no_interiteration ,否则它会认为存在一个interiteration循环进行的依赖关系,并认为循环会在NHM上花费12c。

    链接地址: http://www.djcxy.com/p/28767.html

    上一篇: Why are mov ah,bh and mov al, bl together much faster than single instruction mov ax, bx?

    下一篇: What is the best way to set a register to zero in x86 assembly: xor, mov or and?