【问题标题】:Optimal ordering of memory read and write assembly instructions内存读写汇编指令的最优排序
【发布时间】:2015-05-08 22:23:08
【问题描述】:

我想知道在 Core 2 和 Westmere 之间的英特尔处理器上,如下所示的一系列指令的最佳顺序是什么。这是 AT&T 语法,因此 pxor 指令是内存读取,movdqa 是内存写入:

    movdqa  %xmm0, -128+64(%rbx)
    movdqa  %xmm1, -128+80(%rbx)
    movdqa  %xmm2, -128+96(%rbx)
    movdqa  %xmm3, -128+112(%rbx)
    pxor    -128(%rsp), %xmm0
    pxor    -112(%rsp), %xmm1
    pxor    -96(%rsp), %xmm2
    pxor    -80(%rsp), %xmm3
    movdqa  %xmm8, 64(%rbx)
    movdqa  %xmm9, 80(%rbx)
    movdqa  %xmm10, 96(%rbx)
    movdqa  %xmm11, 112(%rbx)
    pxor    -128(%r14), %xmm8
    pxor    -112(%r14), %xmm9
    pxor    -96(%r14), %xmm10
    pxor    -80(%r14), %xmm11
    movdqa  %xmm12, 64(%rdx)
    movdqa  %xmm13, 80(%rdx)
    movdqa  %xmm14, 96(%rdx)
    movdqa  %xmm15, 112(%rdx)
    pxor    0(%r14), %xmm12
    pxor    16(%r14), %xmm13
    pxor    32(%r14), %xmm14
    pxor    48(%r14), %xmm15

%r14%rsp%rdx%rbx 是 256 的不同倍数。换句话说,上面的指令中没有不明显的别名,并且已经为对齐访问布置了数据大数据块。所有被访问的内存行都在 L1 缓存中。

一方面,我对 Agner Fog 的 optimization guides 的理解让我相信,通过如下顺序的顺序,可以按循环接近两条指令:

movdqa  %xmm0, -128+64(%rbx)
movdqa  %xmm1, -128+80(%rbx)
pxor    -128(%rsp), %xmm0
movdqa  %xmm2, -128+96(%rbx)
pxor    -112(%rsp), %xmm1
movdqa  %xmm3, -128+112(%rbx)
pxor    -96(%rsp), %xmm2
movdqa  %xmm8, 64(%rbx)
pxor    -80(%rsp), %xmm3
movdqa  %xmm9, 80(%rbx)
pxor    -128(%r14), %xmm8
movdqa  %xmm10, 96(%rbx)
pxor    -112(%r14), %xmm9
movdqa  %xmm11, 112(%rbx)
pxor    -96(%r14), %xmm10
movdqa  %xmm12, 64(%rdx)
pxor    -80(%r14), %xmm11
movdqa  %xmm13, 80(%rdx)
pxor    0(%r14), %xmm12
movdqa  %xmm14, 96(%rdx)
pxor    16(%r14), %xmm13
movdqa  %xmm15, 112(%rdx)
pxor    32(%r14), %xmm14
pxor    48(%r14), %xmm15 

此排序尝试通过在读取和写入之间留下偏移量来考虑 Agner Fog 的 microachitecture.pdf 中所述的“缓存库冲突”。

另一方面,另一个担忧是,尽管程序员知道上面的代码中没有别名,但他们无法将这些信息传达给处理器。读取和写入的交错是否会引入延迟,因为处理器必须考虑读取的值可能被上述指令中的写入修改?在这种情况下,显然最好先执行所有读取操作,但由于对于特定的指令序列来说这是不可能的,所以首先完成所有写入操作可能是有意义的。

简而言之,这里似乎有很多可能性,我的直觉还不够好,无法大致了解每种可能性会发生什么。

编辑:如果这很重要,在考虑的序列之前的代码要么从内存加载xmm 寄存器,要么用算术指令计算它们,之后的代码使用这些寄存器将它们写入内存或作为算术指令的输入。已写入的内存位置不会立即重用。 rbxrspr14rdx 是必须来自寄存器文件的长寿命寄存器。

【问题讨论】:

  • 这就是为什么使用内在函数编码 SIMD 通常比编写原始 asm 更好的几个原因之一 - 让编译器为你做指令调度等 - 这样你甚至可以重新编译不同的架构并为每个架构获取最佳代码。
  • @PaulR 我已经阅读了足够多的非 SIMD 编译器生成的汇编代码,确信在看起来合理的选择中选择最差的仍然可能比编译器生成的要好。
  • 也许,但 SIMD 是一个相当不同的场景 - 每个 infrinsic 通常映射到单个指令,因此代码生成器只需要处理指令调度、寄存器分配、窥视孔优化等 - 通常大多数编译器在这方面做得很好,所以对于任何不平凡的事情,我发现 Intrinsics 会产生非常紧凑的代码。当然是 YMMV。
  • @PaulR 对于我正在考虑的那种代码,您有两个选择:处理几个小数组,这有助于编译器使用别名,但意味着它要么耗尽其寄存器,要么生成 9 字节指令大偏移量,这意味着两个永远不会在一个周期内执行。或者使用一个大数组,这意味着编译器对别名无能为力,必须使用源代码的顺序。
  • @PaulR 我正在查看的代码是 github.com/pooler/cpuminer/blob/master/scrypt-x64.S 中的 scrypt_core_3way 已经有些收紧的版本。如果您认为编译器可以做得更好,请说明如何。许多人会感兴趣。

标签: optimization assembly x86-64 sse2


【解决方案1】:

我对我感兴趣的指令和周围的指令进行了如下测试,以测量在使用指令的上下文中不同排序选项所占用的周期数:

#ifdef M    
    push    %rdx
    push    %rax
    push    %rbx
    push    %rcx    
    xorq    %rax, %rax
    cpuid
    rdtsc
    movl    %eax, 256+32+UNUSED_64b
    movl    %edx, 256+32+4+UNUSED_64b
    pop     %rcx        
    pop     %rbx
    pop %rax
    pop %rdx
#endif  
    movdqa  %xmm0, -128+64(%rbx)
    movdqa  %xmm1, -128+80(%rbx)
    movdqa  %xmm2, -128+96(%rbx)
    movdqa  %xmm3, -128+112(%rbx)

    movdqa  %xmm8, 64(%rbx)
    movdqa  %xmm9, 80(%rbx)
    movdqa  %xmm10, 96(%rbx)
    movdqa  %xmm11, 112(%rbx)

    pxor    -128(%rsp), %xmm0   
    pxor    -112(%rsp), %xmm1
    pxor    -96(%rsp), %xmm2    
    pxor    -80(%rsp), %xmm3

    movdqa  %xmm12, 64(%rdx)
    movdqa  %xmm13, 80(%rdx)
    movdqa  %xmm14, 96(%rdx)
    movdqa  %xmm15, 112(%rdx)

    pxor    -128(%r14), %xmm8   
    pxor    -112(%r14), %xmm9
    pxor    -96(%r14), %xmm10
    pxor    -80(%r14), %xmm11

    movdqa  %xmm0, -128+0(%rbx)
    movdqa  %xmm1, -128+16(%rbx)
    movdqa  %xmm2, -128+32(%rbx)
    movdqa  %xmm3, -128+48(%rbx)

    pxor    0(%r14), %xmm12
    pxor    16(%r14), %xmm13
    pxor    32(%r14), %xmm14
    pxor    48(%r14), %xmm15

    movdqa  %xmm8, 0(%rbx)
    movdqa  %xmm9, 16(%rbx)
    movdqa  %xmm10, 32(%rbx)
    movdqa  %xmm11, 48(%rbx)
    movdqa  %xmm12, 0(%rdx)
    movdqa  %xmm13, 16(%rdx)
    movdqa  %xmm14, 32(%rdx)
    movdqa  %xmm15, 48(%rdx)

#ifdef M        
    push    %rdx
    push    %rax
    push    %rbx
    push    %rcx    
    xorq    %rax, %rax
    cpuid   
    rdtsc
    shlq    $32, %rdx
    orq %rdx, %rax
    subq    256+32+UNUSED_64b, %rax
    movq    %rax, 256+32+UNUSED_64b
    pop     %rcx        
    pop     %rbx    
    pop %rax
    pop %rdx
#endif
…
// safe place
    call do_debug
…
#ifdef M
    .cstring
measure:
        .ascii "%15lu\12\0"

        .section        __DATA,__data
    .align 2

count:
    .word 30000

    .text
do_measure:
    decb    count(%rip)
    jnz     done_measure
    pushq   %rax
    pushq   %rax    
    pushq   %rbx
    pushq   %rcx
    pushq   %rdx
    pushq   %rsi
    pushq   %rdi
    pushq   %rbp    
    pushq   %r9
    pushq   %r10
    pushq   %r11
    pushq   %r12
    pushq   %r13
    pushq   %r14
    pushq   %r15

        movq    16*8+UNUSED_64b, %rsi
        leaq    measure(%rip), %rdi
        xorl    %eax, %eax
        call    _applog

    popq    %r15
    popq    %r14
    popq    %r13    
    popq    %r12
    popq    %r11
    popq    %r10    
    popq    %r9
    popq    %rbp    
    popq    %rdi
    popq    %rsi
    popq    %rdx
    popq    %rcx
    popq    %rbx
    popq    %rax
    popq    %rax    
done_measure:
    ret
#endif

上面的序列是我发现对于我正在开发的处理器(Westmere Xeon W3680)更快的序列。我在问题中提出的顺序结果很糟糕,例如,可能是因为它在下面使用 xmm 寄存器的指令和最后设置它们的指令之间放置了太大的距离,迫使它们通过寄存器文件,并导致寄存器读取停顿。

UNUSED_64b 是由于对齐约束而在堆栈上可用的空槽的名称。它必须在堆栈上,因为程序使用线程:

#define UNUSED_64b         16(%rsp) 

256+32+ 补偿了在设置探针的点对堆栈的异常使用。

此汇编代码适用于 Mac OS X。某些细节在另一个类 Unix 系统上会有所不同。

【讨论】:

    猜你喜欢
    • 2013-08-11
    • 1970-01-01
    • 2023-03-10
    • 2014-06-19
    • 1970-01-01
    • 1970-01-01
    • 2015-10-27
    • 2011-09-21
    相关资源
    最近更新 更多