是否值得费心对齐 AVX-256 内存存储?

问题描述 投票:0回答:1

根据英特尔® 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
等等...

如果我正确理解了 Intel 文档中有关页面大小的内容,如果我跨 4K 页边界进行 32 字节存储,我将受到 150 个周期的惩罚。

但是由于块已经按 16 字节对齐,因此我访问跨页存储的机会是 16/4096 = 1/256。如果我们统计推断,在每个 32 字节存储上,我在 Sandy Bridge 上会受到 1/256*150 (=0.5859375) 个周期的惩罚。

这并没有那么多,而且肯定比分支检查对齐要便宜,或者由于将对齐从 16 字节更改为 32 字节而造成内存浪费。

我有以下问题:

  1. 我的计算正确吗?

  2. 考虑到受到惩罚的机会如此之低,对于小型固定大小的内存复制例程(32-128 字节)来说,对齐 AVX-256 内存存储值得烦恼吗?

  3. 是否存在比 Sandy Bridge 具有更高未对齐 32 字节存储惩罚的处理器(例如 AMD 或其他 Intel 微架构)?

performance assembly x86-64 memory-alignment avx
1个回答
13
投票

是否值得费心去对齐[...]?

是的,绝对值得,而且也很便宜。

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

//assume rcx = length of block, assume length > 8.
//assume rdx = pointer to block
xor rax,rax
mov r9,rdx         //store block pointer 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

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

对齐是一个简单的问题

misalignment= start and not(alignmentsize -1)

然后,您可以执行
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}

这并没有那么多,而且绝对比分支检查对齐更便宜
我认为支票相当便宜(见上文)。 请注意,您可能会遇到一直受到处罚的病态情况,因为这些块碰巧经常横跨线路。

关于混合 AVX 和 SSE 代码
在 Intel 上,混合 AVX 和(传统的,即非 VEX 编码的)SSE 指令会导致 300 多个周期的损失。
如果您使用 AVX2 指令写入内存,并且在应用程序的其余部分中使用 SSE 代码,并且 Delphi 64 仅将 SSE 用于浮点,则会受到惩罚。
在这种情况下使用 AVX2 代码会导致严重的延迟。仅出于这个原因,我建议您不要考虑 AVX2。

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

为什么混合 AVX 和 SSE(遗留)代码会受到惩罚?
英特尔写道:

最初处理器处于干净状态 (1),其中 Intel SSE 和 执行英特尔 AVX 指令不会造成任何损失。当 256 位 Intel AVX指令被执行,处理器标记它处于 脏上层状态 (2)。在此状态下,执行 Intel SSE 指令保存所有YMM寄存器的高128位和 状态更改为 Saved Dirty Upper 状态 (3)。下次使用英特尔 AVX 指令执行后所有YMM寄存器的高128位为 恢复并且处理器回到状态(2)。这些保存并 恢复操作有很高的惩罚。经常执行这些 转换会导致显着的性能损失。

还有暗硅的问题。 AVX2 代码使用大量硬件,点亮所有芯片会消耗大量功率,从而影响热裕量。执行 AVX2 代码时,CPU 会降低速度,有时甚至低于正常的非涡轮阈值。通过关闭 256 位 AVX 电路,CPU 可以获得更高的睿频时钟,因为具有更好的热余量。 AVX2 电路的关闭开关在较长的持续时间 (675us) 内看不到 256 位代码,而打开开关则看到 AVX2 代码。将两者混合会导致电路的打开和关闭,这需要多个周期。

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