Векторизация с неуравновешенными буферами: использование VMASKMOVPS: создание маски из подсчета несоосности? Или не использовать этот insn вообще

gcc 5.3 с -O3 -mavx -mtune=haswell для x86-64 делает удивительно громоздкий код для обработки потенциально несогласованных входов для кода типа:

 // 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 использует нестандартные команды load / store, но 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. Я сообщил об ошибке gcc об этом, предложив использовать более мелкий / лучший код, который gcc мог бы использовать при отслаивании неустановленных итераций. Это, вероятно, все еще не оптимально.

Этот вопрос касается того, что на самом деле было бы оптимальным с AVX . Я прошу об общих решениях, которые gcc и другие компиляторы могли / должны использовать. (Я не нашел ни одного списка рассылки gcc с обсуждением об этом, но долго не смотрел.)


Вероятно, будет много ответов, так как оптимальное для -mtune=haswell , вероятно, будет отличаться от того, что оптимально для -mtune=bdver3 (каток). И тогда возникает вопрос, что оптимально, если разрешить расширения набора инструкций (например, AVX2 для 256b целочисленного материала, BMI1 для перевода счетчика в битмаску в меньшем количестве инструкций).

Я знаю руководство Aggress Fog’s Optimization Assembly, раздел 13.5 «Доступ к неглавным данным и частичным векторам» . Он предлагает либо использовать неприглаженные обращения, делая перекрывающуюся запись в начале и / или конце, либо перетасовывая данные из выровненных доступов (но PALIGNR принимает только число imm8, поэтому 2x pshufb / por ). Он VMASKMOVPS как не полезный, возможно, из-за того, насколько плохо он работает на AMD. Я подозреваю, что при настройке на Intel это стоит рассмотреть. Непонятно, как создать правильную маску, поэтому заголовок вопроса.


Может оказаться, что лучше просто использовать неприглаженные обращения, как это делает clang. Для коротких буферов накладные расходы на выравнивание могут убьют любую выгоду от избежания расщепления кешлин для основного цикла. Для больших буферов, основной памяти или L3, поскольку узкое место может скрывать штраф за каскадные расщепления. Если у кого-то есть экспериментальные данные, чтобы поддержать это для любого реального кода, который они настроили, это тоже полезная информация.


VMASKMOVPS действительно подходит для целей Intel. (Версия SSE ужасна, с неявным невременным намеком, но в версии AVX этого нет. Существует даже новая возможность убедиться, что вы не получите версию SSE для 128b-операндов: _mm128_maskstore_ps ) Версия AVX только немного медленнее на Хасуэлле :

  • 3 uops / 4c latency / 1-per-2c в качестве нагрузки.
  • Пропускная способность 4 мк / 14 с / 1 на 2с в качестве хранилища 256b.
  • 4 uops / 13c latency / 1-per-1c в качестве хранилища 128b.

Форма магазина по-прежнему неэффективна на процессорах AMD, как Jaguar (1 на 22c tput), так и Bulldozer-family: 1 на 16c на Steamroller (аналогично на Bulldozer) или 1 на ~ 180c пропускную способность на Piledriver.

Но если мы хотим использовать VMASKMOVPS , нам нужен вектор с высоким битом, установленным в каждом элементе, который должен быть фактически загружен / сохранен. PALIGNR и PSRLDQ (для использования на векторе all-ones) принимают только значения постоянной времени компиляции.

Обратите внимание, что другие биты не имеют значения: они не обязательно должны быть однотипными, поэтому возможность рассеивания некоторых битов элемента с высокими битами элементов является возможностью.

Загрузите маску для VMOVMASKPS из windows в таблицу. AVX2 или AVX1 с несколькими дополнительными инструкциями или большой таблицей.

Маска также может использоваться для ANDPS в регистрах в сокращении, которое должно подсчитывать каждый элемент ровно один раз. Как отмечает Стивен Канон в комментариях к OP, конвейерные нагрузки могут позволить перекрывать неравномерные магазины для работы даже для функции перезаписи на месте, как в примере, который я выбрал, поэтому VMASKMOVPS НЕ является лучшим выбором здесь.


Это должно быть хорошо на процессорах Intel, особенно. 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)
  • start misalign count = 7: mask = {0, 0, 0, 0, 0, 0, 0,-1} (пропустите все, кроме одного в первом 32B)

  • end misalign count = 0: никаких конечных элементов. mask = all-ones (Aligned case).
    это нечетный случай, не похожий на count = 1 . Пара дополнительных инструкций для этого особого случая стоит избегать дополнительной циклической итерации цикла и очистки с маской all-zeros.

  • end misalign count = 1: один конечный элемент. mask = {-1, 0, 0, 0, 0, 0, 0, 0}
  • конечный неверный счет = 7: семь задних элем. mask = {-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-х инструкций для генерации счетчика несоосности и выровненного начального адреса, получение маски принимает только одну команду vpmosvsxbd. (Форма ymm, mem не может быть микро-предохранителем, так что это 2 uops). Для этого требуется AVX2.


Без AVX2:

  • 2x vpmovsxbd на два 128b регистра ( [masktable_intro + rax] и [masktable_intro + rax + 4] )
  • vinsertf128

Или: (больше insns и больше давления в шунтировании, но меньшее давление нагрузки)

  • vpmovsxbw в регистр 128b
  • vpunpcklwd / vpunpckhwd в две xmm regs (src1 = src2 для обоих)
  • vinsertf128

Или:

  • vmovdqu из таблицы 60B DWORDs ( DD ) вместо байтов ( DB ). Это фактически спасло бы insn относительно AVX2: address & 0x1c - это индекс, без необходимости сдвига вправо на два. Вся таблица по-прежнему подходит для строки кэша, но без места для других констант, которые может использовать алго.

Накладные расходы:

  • Integer ops: 5 uops в начале, чтобы получить индекс и выровнять указатель начала. 7 uops, чтобы получить индекс для конечной маски. Всего 12 GP-регистров, кроме простого использования без выравнивания, если число элементов цикла кратно ширине вектора.

  • AVX2: Два векторных insns 2-fused-domain-uop для перехода от индекса [0..7] в GP reg к маске в регистре YMM. (Один для стартовой маски, один для маски конца). Использует таблицу 24B, доступную в окне 8B с детализацией байт.

  • AVX: шесть векторных insns с 1-мя плавким доменом (три в начале, три в конце). С RIP-относительной адресацией для таблицы четыре из этих инструкций будут [base+index] и не будут микшироваться, поэтому дополнительные два целых insns могут быть лучше.

Код внутри цикла реплицируется 3 раза.


TODO: напишите еще один ответ, создающий маску «на лету», возможно, как байты в 64b reg, а затем распакуйте ее до 256b. Может быть, с бит-сдвигом, или BMI2's BZHI (-1, count)?

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 

Выполнение нагрузки с конца массива в начале цикла кажется немного странным, но, надеюсь, оно не путает аппаратные префетеры или замедляет процесс начала streamа массивов из памяти.

Накладные расходы:

  • 2 дополнительных целочисленных значения uops total (для настройки выровненного начала). Мы уже используем конечный указатель для нормальной структуры цикла, так что это бесплатно.

  • 2 дополнительных копии тела цикла (load / calc / store). (Первая и последняя итерация очищена).


Составители, вероятно, не будут довольны выдачей кода, подобного этому, при автоиндексировании. Valgrind сообщит о доступе за пределами границ массива и делает это с помощью инструкций с одним шагом и расшифровкой, чтобы увидеть, к чему они обращаются. Поэтому просто оставаться на одной странице (и строке кэша), поскольку последний элемент в массиве недостаточен. Также обратите внимание, что если указатель ввода не выравнивается по 4B, мы можем потенциально читать на другой странице и segfault.

Чтобы сохранить Valgrind счастливым, мы могли бы остановить петлю на две ширины вектора раньше, чтобы выполнить загрузку специального блока невязанной последней ширины вектора массива. Это потребует дублирования тела цикла лишнего времени (незначительное в этом примере, но это тривиально по назначению). Или, возможно, избегайте конвейерной обработки, если код intro перейдет в середину цикла. (Это может быть неоптимальным для uop-cache, хотя: (части) тела цикла могут закончиться в кэше uop дважды.)

TODO: напишите версию, которая переходит в цикл в середине.

  • Есть ли у блокировки xchg то же поведение, что и mfence?
  • Как вы используете gcc для генерации кода сборки в синтаксисе Intel?
  • Почему вы не можете установить указатель инструкции напрямую?
  • Инструкция INC против ADD 1: Это имеет значение?
  • Как реализовать atoi с помощью SIMD?
  • Самый быстрый способ сделать горизонтальную векторную сумму float на x86
  • Печать шестнадцатеричных цифр со сборкой
  • Распределение стека, отступы и выравнивание
  • Могу ли я принуждать кеш к многоядерному процессору x86?
  • Улучшенный REP MOVSB ​​для memcpy
  • Что означает «DS: » означает в сборке?
  • Давайте будем гением компьютера.