对齐AVX-256内存存储是否值得打扰?

Max*_*tin 15 memory assembly x86-64 avx avx2

根据英特尔®64和IA-32架构优化参考手册 B.4节("英特尔®微体系架构代码名称Sandy Bridge的性能调整技术"),B.4.5.2小节("协助"):

跨越两页的32字节AVX存储指令需要一个大约150个周期的辅助.

我正在使用YMM寄存器来复制小型固定大小的内存块,从32到128个字节,并且这些块在堆管理器中以16个字节对齐.该堆管理器之前使用过XMM寄存器movdqa,我想将其"升级"为YMM,而不将对齐从16字节更改为32字节.所以我正在使用vmovdqu ymm0, ymmword ptr [rcx],vmovdqu ymmword ptr [rdx], ymm0等等......

如果我正确理解了英特尔文档的页面大小,如果我在4K页面边界上进行32字节存储,那么我将获得150个周期的惩罚.

但由于这些块已经对齐了16个字节,因此我点击跨页面存储的可能性是16/4096 = 1/256.如果我们在统计上推断出,在每个32字节的存储上,我在Sandy Bridge上得到1/255*150(= 0.5859375)个周期惩罚.

这不是那么多,并且由于将对齐从16字节更改为32字节,因此检查对齐或内存浪费肯定比分支更便宜.

我有以下问题:

  1. 我的计算是否正确?

  2. 对齐AVX-256内存存储器是否值得为小型固定大小的内存复制例程(32-128字节)而烦恼,因为达到惩罚的可能性如此之低?

  3. 是否存在比Sandy Bridge更高的未对齐32字节存储损失的处理器 - 例如,AMD或其他英特尔微体系结构?

Joh*_*ica 9

对齐[...]是否值得打扰?

是的,绝对值得,它也很便宜.

您可以轻松地对未对齐块进行对齐写入,而无需跳转.
例如:

//assume rcx = length of block, assume length > 8.
//assume rdx = pointer to block
xor rax,rax
mov r9,rdx         //remember r9 for later
sub rcx,8           
mov [rdx],rax      //start with an unaligned write
and rdx,not(7)     //force alignment
lea r8,[rdx+rcx]   //finish with unaligned tail write
xor r9,rdx         //Get the misaligned byte count.
sub rcx,r9
jl @tail           //jl and fuse with sub
@loop:
  mov [rdx],rax    //all writes in this block are aligned.
  lea rdx,[rdx+8]  
  sub rcx,8
  jns @loop
@tail 
mov [r8],rax       //unaligned tail write
Run Code Online (Sandbox Code Playgroud)

我确信您可以将此示例从非展开示例推断为优化的AVX2示例.

对齐是一个简单的问题misalignment= start and not(alignmentsize -1).
然后,您可以执行a misalignmentcount = start xor misalingment来计算未对齐的字节数.

这些都不需要跳跃.
我相信你可以把它翻译成AVX.

以下代码FillChar比标准库快3倍左右.
请注意,我已经使用了跳转,测试显示它更快.

{$ifdef CPUX64}
procedure FillChar(var Dest; Count: NativeInt; Value: Byte);
//rcx = dest
//rdx=count
//r8b=value
asm
              .noframe
              .align 16
              movzx r8,r8b           //There's no need to optimize for count <= 3
              mov rax,$0101010101010101
              mov r9d,edx
              imul rax,r8            //fill rax with value.
              cmp edx,59             //Use simple code for small blocks.
              jl  @Below32
@Above32:     mov r11,rcx
              rep mov r8b,7          //code shrink to help alignment.
              lea r9,[rcx+rdx]       //r9=end of array
              sub rdx,8
              rep mov [rcx],rax      //unaligned write to start of block
              add rcx,8              //progress 8 bytes 
              and r11,r8             //is count > 8? 
              jz @tail
@NotAligned:  xor rcx,r11            //align dest
              lea rdx,[rdx+r11]
@tail:        test r9,r8             //and 7 is tail aligned?
              jz @alignOK
@tailwrite:   mov [r9-8],rax         //no, we need to do a tail write
              and r9,r8              //and 7
              sub rdx,r9             //dec(count, tailcount)
@alignOK:     mov r10,rdx
              and edx,(32+16+8)      //count the partial iterations of the loop
              mov r8b,64             //code shrink to help alignment.
              mov r9,rdx
              jz @Initloop64
@partialloop: shr r9,1              //every instruction is 4 bytes
              lea r11,[rip + @partial +(4*7)] //start at the end of the loop
              sub r11,r9            //step back as needed
              add rcx,rdx            //add the partial loop count to dest
              cmp r10,r8             //do we need to do more loops?
              jmp r11                //do a partial loop
@Initloop64:  shr r10,6              //any work left?
              jz @done               //no, return
              mov rdx,r10
              shr r10,(19-6)         //use non-temporal move for > 512kb
              jnz @InitFillHuge
@Doloop64:    add rcx,r8
              dec edx
              mov [rcx-64+00H],rax
              mov [rcx-64+08H],rax
              mov [rcx-64+10H],rax
              mov [rcx-64+18H],rax
              mov [rcx-64+20H],rax
              mov [rcx-64+28H],rax
              mov [rcx-64+30H],rax
              mov [rcx-64+38H],rax
              jnz @DoLoop64
@done:        rep ret
              //db $66,$66,$0f,$1f,$44,$00,$00 //nop7
@partial:     mov [rcx-64+08H],rax
              mov [rcx-64+10H],rax
              mov [rcx-64+18H],rax
              mov [rcx-64+20H],rax
              mov [rcx-64+28H],rax
              mov [rcx-64+30H],rax
              mov [rcx-64+38H],rax
              jge @Initloop64        //are we done with all loops?
              rep ret
              db $0F,$1F,$40,$00
@InitFillHuge:
@FillHuge:    add rcx,r8
              dec rdx
              db $48,$0F,$C3,$41,$C0 // movnti  [rcx-64+00H],rax
              db $48,$0F,$C3,$41,$C8 // movnti  [rcx-64+08H],rax
              db $48,$0F,$C3,$41,$D0 // movnti  [rcx-64+10H],rax
              db $48,$0F,$C3,$41,$D8 // movnti  [rcx-64+18H],rax
              db $48,$0F,$C3,$41,$E0 // movnti  [rcx-64+20H],rax
              db $48,$0F,$C3,$41,$E8 // movnti  [rcx-64+28H],rax
              db $48,$0F,$C3,$41,$F0 // movnti  [rcx-64+30H],rax
              db $48,$0F,$C3,$41,$F8 // movnti  [rcx-64+38H],rax
              jnz @FillHuge
@donefillhuge:mfence
              rep ret
              db $0F,$1F,$44,$00,$00  //db $0F,$1F,$40,$00
@Below32:     and  r9d,not(3)
              jz @SizeIs3
@FillTail:    sub   edx,4
              lea   r10,[rip + @SmallFill + (15*4)]
              sub   r10,r9
              jmp   r10
@SmallFill:   rep mov [rcx+56], eax
              rep mov [rcx+52], eax
              rep mov [rcx+48], eax
              rep mov [rcx+44], eax
              rep mov [rcx+40], eax
              rep mov [rcx+36], eax
              rep mov [rcx+32], eax
              rep mov [rcx+28], eax
              rep mov [rcx+24], eax
              rep mov [rcx+20], eax
              rep mov [rcx+16], eax
              rep mov [rcx+12], eax
              rep mov [rcx+08], eax
              rep mov [rcx+04], eax
              mov [rcx],eax
@Fallthough:  mov [rcx+rdx],eax  //unaligned write to fix up tail
              rep ret

@SizeIs3:     shl edx,2           //r9 <= 3  r9*4
              lea r10,[rip + @do3 + (4*3)]
              sub r10,rdx
              jmp r10
@do3:         rep mov [rcx+2],al
@do2:         mov [rcx],ax
              ret
@do1:         mov [rcx],al
              rep ret
@do0:         rep ret
end;
{$endif}
Run Code Online (Sandbox Code Playgroud)

这不是那么多,并且肯定比分支检查对齐更便宜
我认为检查非常便宜(见上文).请注意,您可能会遇到一直受到惩罚的病态情况,因为这些障碍恰好跨越了很多行.

关于混合AVX和SSE代码
在英特尔,混合AVX和(传统的,即非VEX编码的)SSE指令有300多周期的惩罚.
如果您使用AVX2指令写入内存,如果在应用程序的其余部分使用SSE代码,则会受到惩罚,而Delphi 64仅使用SSE作为浮点数.
在这种情况下使用AVX2代码会导致严重的延迟.仅此因此我建议您不要考虑AVX2.

不需要AVX2
您可以使用仅写入的64位通用寄存器来使内存总线饱和.
在进行组合读写操作时,128位读写操作也很容易使总线饱和.
在较旧的处理器上也是如此,如果超出L1缓存,显然也是如此,但在最新的处理器上则不然.

为什么混合AVX和SSE(传统)代码会受到惩罚?
英特尔撰写以下内容:

最初处理器处于干净状态(1),执行英特尔SSE和英特尔AVX指令时不会受到任何惩罚.执行256位Intel AVX指令时,处理器会标记它处于Dirty Upper状态(2).在此状态下,执行Intel SSE指令会保存所有YMM寄存器的高128位,并且状态将更改为Saved Dirty Upper状态(3).下次执行Intel AVX指令时,将恢复所有YMM寄存器的高128位,处理器返回状态(2).这些保存和恢复操作具有很高的惩罚.频繁执行这些转换会导致严重的性能损失.

还存在暗硅问题.AVX2代码使用了大量的硬件,所有硅点亮使用了大量的功率,影响了热量余量.执行AVX2代码时,CPU会降低压力,有时甚至低于正常的非涡轮阈值.通过关闭256位AVX的电路,CPU可以实现更高的turbo时钟,因为它具有更好的热余量.AVX2电路的关闭开关在很长一段时间内(675us)没有看到256位代码,并且开关正在看到AVX2代码.将两者混合会导致电路的接通和断开,这需要多次循环.

  • @MaximMasiutin约翰关于能够用正常单词大小饱和所有内容的评论在5年前是真实的(在Sandy Bridge上).当然,事情发生了变化.所以他没有错,只是对你的Kaby湖来说已经过时了.现在,即使你全力以赴SIMD,有时也不可能用单核来使内存总线饱和.对于四通道内存,您需要2个或更多内核 - 即使是256位AVX. (3认同)
  • @MaximMasiutin 如果分支是一个问题,那么基准测试没有正确实现。一切都应该展开到分支无关紧要的地方。 (2认同)