使用未对齐的缓冲区矢量化:使用VMASKMOVPS:从未对齐计数生成掩码吗?还是根本不使用该insn

问题描述 投票:11回答:2

x86-64的[g0带有-O3 -mavx -mtune=haswell的gcc 5.3使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执行标量引入/输出和对齐的矢量循环:它剥离了最初的多达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在剥离未对齐的迭代时可以使用的更小/更好的代码的建议。不过,它可能仍然不是最佳选择。

这个问题是关于使用AVX实际是什么最佳]。我在问gcc和其他编译器可以/应该使用的一般情况的解决方案。 (我没有找到有关此问题的讨论的gcc邮件列表命中,但没有花很长时间。)


可能会有多个答案,因为对-mtune=haswell的最佳选择可能与对-mtune=bdver3(压路机)的最佳选择不同。然后是一个问题,当允许指令集扩展时(例如,AVX2用于256b整数填充,BMI1用于在更少的指令中将计数转换为位掩码)是最佳的。

我知道Agner Fog的《优化装配》指南,第13.5节,访问未对齐的数据和局部矢量。他建议要么使用未对齐的访问,在开始和/或结束处进行重叠的写操作,要么从对齐的访问中整理数据(但PALIGNR仅占用imm8计数,所以2xpshufb/ por)。他认为VMASKMOVPS没用,可能是因为它在AMD上的表现很差。我怀疑如果针对英特尔进行调优,则值得考虑。如何生成正确的遮罩并因此产生问题标题尚不明显。


可能会发现,最好像clang一样简单地使用未对齐的访问。对于短缓冲区,对齐的开销可能会扼杀避免在主循环中避免缓存行拆分的任何好处。对于大缓冲区,作为瓶颈的主内存或L3可能会隐藏高速缓存行拆分的代价。如果有人拥有实验数据来支持他们调整过的任何真实代码,那也是有用的信息。


VMASKMOVPS确实对英特尔目标有用。 (SSE版本太可怕了,带有隐式的非时间提示,但AVX版本没有该提示。甚至还有一个新的内在函数可以确保您不会获得128b操作数的SSE版本:_mm128_maskstore_ps) AVX版本为only a little bit slow on Haswell

  • 3微秒/ 4c延迟/每2c 1个吞吐量作为负载。
  • 4 uops / 14c延迟/ 1 /每2c吞吐量作为256b存储。
  • 4微码/ 13c延迟/每1c 1个吞吐量(作为128b存储。] >>

    [在Jaguar(每22c tput 1个)和Bulldozer系列上的AMD CPU上,存储形式仍然非常缓慢,在Steamroller(与Bulldozer相似)上每16c 1个,或者在PILEDRIVER上每180c吞吐1个]]

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

  • 请注意,其他位无所谓:不必全为1,因此有可能将一些设置位散布到元素的高位。

    用于x86-64的带有-O3 -mavx -mtune = haswell的gcc 5.3使代码出奇的庞大,无法处理代码的可能未对齐的输入,例如://编译器输入的便捷简单示例//我不是...

您可以使用(1 << (vector1.size() & 3)) - 1来将is there an inverse instruction to the movemask instruction in intel avx2?之类的整数位掩码转换为矢量掩码,或者:

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

掩码也可以用于寄存器中的ANDPS,以减少需要精确计数每个元素一次的情况。正如Stephen Canon在关于OP的评论中指出的那样,流水线加载可以允许重叠的未对齐存储区工作,即使对于像我选择的示例那样的就地重写功能,因此VMASKMOVPS在这里是[[NOT

的最佳选择。
这在Intel CPU上应该不错,尤其是Haswell和更高版本的AVX2。

Agner Fog的获取pshufb掩码的方法实际上提供了一个非常有效的想法:执行未对齐的加载以从表中获取数据窗口。与其使用庞大的掩码表,不如使用索引来对内存中的数据进行字节移位。


掩码以LSB-第一个字节顺序(因为它们存储在内存中),而不是向量中{X3,X2,X1,X0}元素的常用符号。如所写,它们与一个对齐的窗口对齐,包括内存中输入数组的开始/结束。

    开始未对齐计数= 0:掩码=全1(对齐的情况)
  • 开始未对齐计数= 1:掩码= {0,-1,-1,-1,-1,-1,-1,-1}(在前32B中跳过一个)
  • ...
  • 开始未对齐计数= 7:掩码= {0, 0, 0, 0, 0, 0, 0,-1}(在前32B中跳过所有,但只跳过一个)

  • end misalign count = 0:没有尾随元素。 mask =全1(对齐的情况)。

    这是奇怪的情况,与count = 1不相似

  • 。对于这种特殊情况,有一些额外的说明值得避免额外的循环迭代和使用全零掩码的清理。最终未对齐数= 1:一个尾随元素。遮罩= {-1, 0, 0, 0, 0, 0, 0, 0}
  • ...
  • 末端未对齐数= 7:七个尾部元素。遮罩= {-1,-1,-1,-1,-1,-1,-1, 0}
  • 未经测试的代码,假设有错误

  • 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条指令之后,仅通过

    single

vpmosvsxbd指令即可获得掩码。 (ymm,mem形式不能微熔,所以是2 oups)。这需要AVX2。
没有AVX2:

    vmovdqu来自DWORD(DD)而不是字节(DB)的60B表。实际上,这将相对于AVX2
  • save一个insn:address & 0x1c是索引,而无需向右移动两位。整个表仍然适合高速缓存行,但是算法没有空间容纳其他常量。

或:

    将2x vpmovsxbd放入两个128b寄存器([masktable_intro + rax][masktable_intro + rax + 4]
  • vinsertf128
  • 或者:(更多的insns和更多的shuffle-port压力,但是更少的load-port压力,几乎可以肯定会更糟)

      vpmovsxbw进入128b寄存器
    • 将vpunpcklwd / vpunpckhwd分为两个xmm regs(两者均为src1 = src2)
    • vinsertf128

  • 开销:

    • Integer ops:在开始时5 oups,以获取索引并对齐开始指针。 7 oups以获得端罩的索引。如果循环元素计数是向量宽度的倍数,则不仅仅使用未对齐,总共还有12个GP寄存器。

    • AVX2:两个2融合域uop向量insns,从GP reg中的[0..7]索引转到YMM reg中的掩码。 (一个用于开始蒙版,一个用于结束蒙版)。使用24B表,该表在8B窗口中以字节粒度访问。

  • AVX:六个1个融合域uop向量insns(开头三个,结尾三个)。使用针对该表的RIP相对寻址,这些指令中的四个将为[base+index],并且不会微熔丝,因此额外的两个整数insns可能会更好。
  • 循环内的代码被复制3次。


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

    仅AVX:开始/结束时未对齐的访问,对负载进行流水线处理,以避免在原地重写时出现问题。

    感谢@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个额外整数,总计(以设置对齐起始位置)。我们已经在普通循环结构中使用了结束指针,所以这是免费的。

  • 2个额外的循环体副本(加载/计算/存储)。 (剥离了第一个和最后一个迭代)。


    当自动向量化时,编译器可能不会对发出这样的代码感到不满意。 Valgrind will report accesses outside of array bounds,并通过单步执行和解码指令来查看它们正在访问的内容。因此,仅停留在与数组中的最后一个元素相同的页面(和缓存行)中是不够的。还请注意,如果输入指针未与4B对齐,则可能会读入另一个页面并出现段错误。

    为了使Valgrind满意,我们可以尽早停止循环

    two向量宽度,以特殊情况加载数组未对齐的最后一个向量宽度。这将需要额外的时间来复制循环主体(在此示例中是微不足道的,但故意这样做是微不足道的。)或者通过将介绍代码跳入循环的中间来避免流水线操作。 (尽管对于uop缓存而言,这可能不是最优的:(部分)循环体可能会在uop缓存中两次出现。)

    TODO:编写一个跳入循环中间的版本。
  • gcc assembly x86 sse avx
    2个回答
    5
    投票

    您可以使用(1 << (vector1.size() & 3)) - 1来将is there an inverse instruction to the movemask instruction in intel avx2?之类的整数位掩码转换为矢量掩码,或者:

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

    掩码也可以用于寄存器中的ANDPS,以减少需要精确计数每个元素一次的情况。正如Stephen Canon在关于OP的评论中指出的那样,流水线加载可以允许重叠的未对齐存储区工作,即使对于像我选择的示例那样的就地重写功能,因此VMASKMOVPS在这里是[[NOT


    1
    投票
    仅AVX:开始/结束时未对齐的访问,对负载进行流水线处理,以避免在原地重写时出现问题。

    感谢@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

    在循环开始时从数组末尾进行加载似乎有些怪异,但希望它不会混淆硬件预取器,或减慢从内存中流式传输数组开始的速度。 

    开销:

    © www.soinside.com 2019 - 2024. All rights reserved.