Стоит ли портить хранилища памяти AVX-256?

В соответствии с Справочным руководством по оптимизации архитектуры Intel® 64 и IA-32, раздел B.4 ( "Методики настройки производительности для Intel архитектуры Microarchitecture Code Name 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 о размере страницы, если я сделаю 32-байтовый магазин на границе 4K-страницы, я получу штраф в 150 циклов.

Но поскольку блоки уже выровнены на 16 байтов, вероятность того, что я попал в хранилище между страницами, равен 16/4096 = 1/256. Если мы статистически экстраполируем это, то в каждом 32-байтовом магазине я получаю 1/256 * 150 (= 0,5859375) штрафных циклов на Sandy Bridge.

Это не так много и определенно дешевле, чем разветвление, чтобы проверить выравнивание или потеря памяти из-за изменения выравнивания от 16 до 32 байтов.

У меня есть следующие вопросы:

  • Правильно ли мои вычисления?

  • Совместимы ли хранилища памяти AVX-256, которые нужно беспокоиться о небольших плагинах памяти с фиксированным размером (32-128 байт), учитывая, что шансы нанести штраф настолько низки?

  • Существуют ли процессоры с более высоким неуравновешенным 32-байтным штрафом за магазин, чем Sandy Bridge, например, AMD или другие микроархитектуры Intel?

Ответы

Ответ 1

Не стоит ли выравнивать [...]?

Да, определенно стоит того, и его тоже очень дешево.

Вы можете легко выполнить выравнивание записи в неравномерный блок без необходимости переходов.
Например:

//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

Я уверен, что вы можете экстраполировать этот пример из нерасширенного примера на оптимизированный пример 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 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 существует ограничение на 300 циклов для микширования кода AVX и SSE.
Если вы используете инструкции AVX2 для записи в память, вы понесете штраф, если вы используете код SSE в остальной части вашего приложения, а Delphi 64 использует SSE исключительно для плавающей запятой.
Использование кода AVX2 в этом контексте повлечет за собой задержки при сбоях. По этой причине я предлагаю вам не рассматривать AVX2.

Нет необходимости в AVX2
Вы можете насытить шину памяти, используя 64-битные регистры общего назначения, которые просто записывают.
При комбинированном чтении и записи 128 бит считывает и записывает также легко насыщает шину.
Дел > Это справедливо для более старых процессоров и, очевидно, также верно, если вы переходите за пределы кеша L1, но не верны на последних процессорах.