首页 文章

使用未对齐缓冲区进行矢量化:使用VMASKMOVPS:根据未对齐计数生成掩码?或者根本不使用那个insn

提问于
浏览
10

gcc 5.3与 -O3 -mavx -mtune=haswell for x86-64使surprisingly bulky code处理代码的潜在错位输入,如:

// convenient simple example of compiler input
// I'm not actually interested in this for any real program
void floatmul(float *a) {
  for (int i=0; i<1024 ; i++)
    a[i] *= 2;
}

clang使用未对齐的加载/存储指令,但是gcc执行标量intro / outro和对齐的向量循环:它剥离了第一个最多7个未对齐的迭代,将其完全展开为一个序列

vmovss  xmm0, DWORD PTR [rdi]
    vaddss  xmm0, xmm0, xmm0      ; multiply by two
    vmovss  DWORD PTR [rdi], xmm0
    cmp     eax, 1
    je      .L13
    vmovss  xmm0, DWORD PTR [rdi+4]
    vaddss  xmm0, xmm0, xmm0
    vmovss  DWORD PTR [rdi+4], xmm0
    cmp     eax, 2
    je      .L14
    ...

这似乎很可怕,尤其是 . 对于具有uop缓存的CPU . 我报告了一个gcc bug关于这一点,并建议gcc在剥离未对齐迭代时可以使用更小/更好的代码 . 不过,它可能仍然不是最优的 .

This question is about what actually would be optimal with AVX . 我发现任何gcc邮件列表都有关于此的讨论,但是没有花太多时间查看 . )


可能会有多个答案,因为 -mtune=haswell 的最佳选择可能与 -mtune=bdver3 (steamroller)的最佳选择不同 . 然后在允许指令集扩展时(例如AVX2用于256b整数填充,BMI1用于将计数转换为更少指令中的位掩码),最佳情况是's the question of what' .

I 'm aware of Agner Fog' s优化装配指南,第13.5节访问未对齐数据和部分向量 . 他建议使用未对齐的访问,在开始和/或结束时进行重叠写入,或者从对齐访问中对数据进行混洗(但是 PALIGNR 只需要一个imm8计数,所以2x pshufb / por ) . 他认为 VMASKMOVPS 没有用,可能是因为它对AMD的表现有多糟糕 . 我怀疑,如果调整英特尔,它's worth considering. It'不明显如何生成正确的掩码,因此问题 Headers .


事实证明,简单地使用未对齐的访问会更好,就像clang那样 . 对于短缓冲区,对齐的开销可能会从避免主循环的cacheline拆分中获得任何好处 . 对于大缓冲区,主内存或L3作为瓶颈可能会隐藏高速缓存行拆分的惩罚 . 如果有人有实验数据支持他们调整的任何真实代码,那也是有用的信息 .


VMASKMOVPS 确实可用于英特尔目标 . (SSE版本非常糟糕,具有隐含的非时间提示,但AVX版本甚至不是一个新的内在函数,以确保您不会获得128b操作数的SSE版本: _mm128_maskstore_ps )AVX版本是only a little bit slow on Haswell

  • 3 uops / 4c延迟/ 1-per-2c吞吐量作为负载 .
    作为256b存储,
  • 4 uops / 14c延迟/ 1-per-2c吞吐量 .
    作为128b存储,
  • 4 uops / 13c延迟/ 1-per-1c吞吐量 .

AMD CPU上的商店形式仍然非常缓慢,包括Jaguar(每22c输出1个)和Bulldozer系列:在Steamroller上每16c 1个(Bulldozer上类似),或者在Piledriver上每1~180c吞吐量1个 .

但是如果我们确实想要使用 VMASKMOVPS ,我们需要一个在每个元素中设置高位的向量,这个向量应该实际加载/存储 . PALIGNR和PSRLDQ(用于全向量的向量)仅采用编译时常量计数 .

请注意,其他位无关紧要:它不必是全1,因此将一些设置位分散到元素的高位是有可能的 .

2 回答

  • 5

    将VMOVMASKPS的掩码从窗口加载到表中 . AVX2或AVX1带有一些额外的指令或更大的表格 .

    掩码还可用于寄存器中的缩减,需要对每个元素进行一次精确计数 . 正如Stephen Canon在对OP的评论中指出的那样,流水线负载可以允许重叠的未对齐存储甚至可以像我选择的示例一样进行就地重写功能,因此 VMASKMOVPS 是这里的最佳选择 .


    这应该适用于英特尔CPU,尤其是英特尔CPU . Haswell和后来的AVX2 .

    Agner Fog获取pshufb掩码的方法实际上提供了一个非常有效的想法:从表中获取一个未对齐的负载来获取数据窗口 . 而不是一个巨大的掩码表,使用索引作为对内存中的数据进行字节移位的方法 .


    屏蔽LSB-第一个字节顺序(因为它们存储在内存中),而不是矢量中 {X3,X2,X1,X0} 元素的常用表示法 . 如上所述,它们与对齐的窗口对齐,包括内存中输入数组的开始/结束 .

    • start misalign count = 0:mask = all-ones(Aligned case)

    • start misalign count = 1:mask = {0,-1,-1,-1,-1,-1,-1,-1} (在第一个32B中跳过一个)

    • ......

    • 开始未对齐计数= 7:mask = {0, 0, 0, 0, 0, 0, 0,-1} (跳过除前一个32B中的一个)

    • 结束错位数= 0:没有尾随元素 . mask = all-ones(对齐的情况) .
      this is the odd case, not similar to count=1 . 这个特殊情况的一些额外指令值得避免额外的循环迭代和使用全零掩码的清理 .

    • end misalign count = 1:一个尾随元素 . mask = {-1, 0, 0, 0, 0, 0, 0, 0}

    • ......

    • 结束错位数= 7:七个尾随元素 . mask = {-1,-1,-1,-1,-1,-1,-1, 0}

    Untested code, assume there are bugs

    section .data
    align 32  ; preferably no cache-line boundaries inside the table
    
    ; byte elements, to be loaded with pmovsx. all-ones sign-extends
        DB  0,  0,  0,  0,  0,  0,  0,  0
    masktable_intro:                      ; index with 0..-7
        DB -1, -1, -1, -1, -1, -1, -1, -1
    masktable_outro:                      ; index with -8(aligned), or -1..-7
        DB  0,  0,  0,  0,  0,  0,  0,  0
    
    ; the very first and last 0 bytes are not needed, since we avoid an all-zero mask.
    
    
    section .text
    global floatmul   ; (float *rdi)
    floatmul:
        mov    eax, edi
        and    eax, 0x1c            ; 0x1c = 7 << 2 = 0b11100
    
        lea    rdx, [rdi + 4096 - 32]  ; one full vector less than the end address (calculated *before* masking for alignment).
                    ;;  replace 4096 with rsi*4 if rsi has the count (in floats, not bytes)
    
        and    rdi, ~0x1c           ; Leave the low 2 bits alone, so this still works on misaligned floats.
    
        shr    eax, 2               ; misalignment-count, in the range [0..7]
    
        neg        rax
        vpmovsxbd  ymm0, [masktable_intro + rax]  ; Won't link on OS X: Need a separate LEA for RIP-relative
    
        vmaskmovps ymm1, ymm0, [rdi]
        vaddps     ymm1, ymm1, ymm1   ; *= 2.0
        vmaskmovps [rdi], ymm0, ymm1
    
        ;;; also prepare the cleanup mask while the table is still hot in L1 cache
    
        ; if the loop count known to be a multiple of the vector width,
        ; the alignment of the end will be the same as the alignment of the start
        ; so we could just invert the mask
        ; vpxor    xmm1, xmm1, xmm1     ; doesn't need an execution unit
        ; vpcmpeqd ymm0, ymm1, ymm0
    
        ; In the more general case: just re-generate the mask from the one-past-the-end addr
        mov    eax, edx
        xor    ecx, ecx      ; prep for setcc
        and    eax, 0x1c     ; sets ZF when aligned
        setz   cl            ; rcx=1 in the aligned special-case, else 0
        shr    eax, 2
        lea    eax, [rax + rcx*8]   ; 1..7, or 8 in the aligned case
        neg    rax
        vpmovsxbd  ymm0, [masktable_outro + rax]
    
    
    .loop:
        add      rdi, 32
        vmovups  ymm1, [rdi]    ; Or vmovaps if you want to fault if the address isn't 4B-aligned
        vaddps   ymm1, ymm1, ymm1   ; *= 2.0
        vmovups  [rdi], ymm1
        cmp      rdi, rdx           ; while( (p+=8) < (start+1024-8) )
        jb    .loop        ; 5 fused-domain uops, yuck.
    
        ; use the outro mask that we generated before the loop for insn scheduling / cache locality reasons.
    
        vmaskmov ymm1, ymm0, [rdi]
        vaddps     ymm1, ymm1, ymm1   ; *= 2.0
        vmaskmovps [rdi], ymm0, ymm1
        ret
    
    
        ; vpcmpeqd ymm1, ymm1, ymm1   ; worse way to invert the mask: dep-chain breaker but still needs an execution unit to make all-ones instead of all-zeros.
        ; vpxor    ymm0, ymm0, ymm1
    

    这确实需要来自表的负载,该表可以在L1高速缓存中丢失,并且需要15B的表数据 . (如果循环计数也是可变的,则为24B,我们必须单独生成结束掩码) .

    无论哪种方式,在生成错位计数和对齐的起始地址的4条指令之后,获取掩码只需要一条vpmosvsxbd指令 . (ymm,mem形式可以't micro-fuse, so it' s 2 uops) . 这需要AVX2 .


    没有AVX2:

    • 2x vpmovsxbd分为两个128b寄存器( [masktable_intro + rax][masktable_intro + rax + 4]

    • vinsertf128

    或者:(更多的insn,更多的shuffle-port压力,但负载端口压力更小)

    • vpmovsxbw成128b寄存器

    • vpunpcklwd / vpunpckhwd分为两个xmm regs(两个src1 = src2)

    • vinsertf128

    要么:

    来自60个DWORD表( DD )的

    • vmovdqu而不是Bytes( DB ) . 这实际上会保存相对于AVX2的insn: address & 0x1c 是索引,不需要右移2 . 整个表仍然适合缓存行,但没有空间可以使用算法的其他常量 .

    开销:

    • 整数运算:在开始时使用5个uop来获取索引并对齐起始指针 . 7 uops获取结束掩码的索引 . 如果循环元素计数是向量宽度的倍数,则除了简单地使用未对齐之外,总共有12个GP寄存器微指令 .

    • AVX2:两个2-fused-domain-uop向量insn从GP reg中的[0..7]索引到YMM reg中的掩码 . (一个用于开始掩码,一个用于结束掩码) . 使用24B表,在具有字节粒度的8B窗口中访问 .

    • AVX:六个1-fused-domain-uop向量insn(三个在开始时,三个在结尾) . 对于表的RIP相对寻址,这些指令中的四个将是 [base+index] 并且不会微融合,因此额外的两个整数insn可能更好 .

    循环内的代码被复制3次 .


    TODO:写一个动态生成掩码的另一个答案,可能是64b reg中的字节,然后将其解压缩到256b . 也许有一个位移,或BMI2的BZHI(-1,计数)?

  • 1

    AVX-only:开始/结束时的未对齐访问,管道加载以避免在重写时出现问题 .

    感谢@StephenCanon指出这比 VMASKMOVPS 更好,因为 VMASKMOVPS 可以帮助循环未对齐的缓冲区 .

    这可能有点期待编译器作为循环转换,特别是 . 因为显而易见的方式可以让Valgrind不高兴(见下文) .

    section .text
    global floatmul   ; (float *rdi)
    floatmul:
    
        lea    rdx, [rdi + 4096 - 32]  ; one full vector less than the end address (calculated *before* masking for alignment).
                    ;;  replace 4096 with rsi*4 if rsi has the count (in floats, not bytes)
    
        vmovups  ymm0, [rdi]
        vaddps   ymm0, ymm0, ymm0   ; *= 2.0
        ; don't store yet
    
        lea    rax, [rdi+32]
        and    rax, ~0x1c           ; 0x1c = 7 << 2 = 0b11100
        vmovups  ymm1, [rax]        ; first aligned vector, for use by first loop iteration
    
        vmovups  [rdi], ymm0        ; store the first unaligned vector
        vmovups  ymm0, [rdx]        ; load the *last* unaligned vector
    
    .loop:
          ;; on entry: [rax] is already loaded into ymm1
        vaddps   ymm1, ymm1, ymm1   ; *= 2.0
        vmovups  [rax]              ; vmovaps would fault if p%4 != 0
        add      rax, 32
        vmovups  ymm1, [rax]
        cmp      rax, rdx           ; while( (p+=8) < (endp-8) );
        jb  .loop
    
        ; discard ymm1.  It includes data from beyond the end of the array (aligned case: same as ymm0)
    
        vaddss   ymm0, ymm0, ymm0   ; the last 32B, which we loaded before the loop
        vmovups  [rdx], ymm0
        ret
    
        ;   End alignment:
        ; a[] = XXXX XXXX ABCD E___    _ = garbage past the end
        ;                  ^rdx
        ;       ^rax ^rax ^rax ^rax(loop exit)
    
        ;   ymm0 = BCDE
        ;   ymm1 loops over ..., XXXX, ABCD, E___
        ;   The last load off the end of the array includes garbage
        ;     because we pipeline the load for the next iteration
    

    在循环开始时从数组末尾执行加载似乎有点奇怪,但希望它不会混淆硬件预取程序,或者减慢从内存中获取数组流的开始 .

    开销:

    总计

    • 2个额外整数uops(设置aligned-start) . 我们已经将结束指针用于正常的循环结构,因此它是免费的 .

    • 2个循环体的额外副本(加载/计算/存储) . (第一次和最后一次迭代去皮) .


    在自动矢量化时,编译器可能不会对发出这样的代码感到高兴 . Valgrind will report accesses outside of array bounds,并通过单步执行和解码指令来查看它们是什么're accessing. So merely staying within the same page (and cache line) as the last element in the array isn'足够了 . 另请注意,如果输入指针不是4B对齐的,我们可能会读入另一个页面和段错误 .

    为了让Valgrind满意,我们可以提前停止循环两个向量宽度,以完成数组未对齐的最后一个向量宽度的特殊情况加载 . 这需要在一个额外的时间内复制循环体(在这个例子中是微不足道的,但是故意这是微不足道的 . )或者可以通过让介绍代码跳到循环的中间来避免流水线操作 . (这对于uop-cache来说可能是次优的,但是:循环体的(部分)可能会在uop缓存中结束两次 . )

    TODO:编写一个在中途跳入循环的版本 .

相关问题