Использование регистров ymm в качестве хранилища "памяти"

рассмотрим следующий цикл в x86:

; on entry, rdi has the number of iterations
.top:
; some magic happens here to calculate a result in rax
mov [array + rdi * 8], rax ; store result in output array
dec rdi
jnz .top

это просто: что-то вычисляет результат в rax (не показано), а затем мы сохраняем результат в массив в обратном порядке, как мы индексируем с rdi.

Я хотел бы преобразовать вышеуказанный цикл, не делая никаких записей в память (мы можем предположить, что не показанный расчет не записывает в память).

покуда отсчет петли в rdi ограничено, я мог бы использовать достаточно места (512 байт), предоставленный ymm regs для сохранения значений вместо этого, но кажется неудобным на самом деле делать это, так как вы не можете "индексировать" произвольный регистр.

одним из подходов было бы всегда перетасовывать весь "массив"ymm регистрируется одним элементом, а затем вставляет элемент во вновь освобожденное положение.

что-то вроде этого:

vpermq  ymm3, ymm3, 10_01_00_11b ; left rotate ymm by qword
vpermq  ymm2, ymm2, 10_01_00_11b ; left rotate ymm by qword
vpermq  ymm1, ymm1, 10_01_00_11b ; left rotate ymm by qword
vpermq  ymm0, ymm0, 10_01_00_11b ; left rotate ymm by qword

vblenddd ymm3, ymm3, ymm2, 3     ; promote one qword of ymm2 to ymm3
vblenddd ymm2, ymm2, ymm1, 3     ; promote one qword of ymm1 to ymm2
vblenddd ymm1, ymm1, ymm0, 3     ; promote one qword of ymm0 to ymm1

pinsrq   xmm0, rax, 0  ; playing with mixed-VEX mode fire (see Peter's answer)

это показывает только обработку четырех из 16 регистров, поэтому очевидно, что для всех 16 будет много кода (32 инструкции.)

есть ли лучший способ?

непредсказуемые ветви нежелательны, но мы все еще можем рассматривать решения, которые их используют.

2 ответов


нельзя vpinsrq в регистр YMM. Доступно только назначение xmm, поэтому оно неизбежно нули верхней полосы полного регистра YMM. Он был представлен с AVX1 в качестве версии VEX 128-битной инструкции. AVX2 и AVX512 не обновили его до назначения YMM/ZMM. Я предполагаю, что они не хотели предоставлять insert в high lanes, и было бы странно предоставить версию YMM, которая все еще только смотрела на самый низкий бит imm8.

вам понадобится регистр царапин, а затем смешаться с YMM с vpblendd. или (на Skylake или AMD) используйте версию legacy-SSE, чтобы оставить верхние байты без изменений! на Skylake запись XMM reg с инструкцией legacy-SSE имеет ложную зависимость от полного регистра. Вы хочу эта ложная зависимость. (Я не тестировал это; это может вызвать слияние uop какого-то рода). Но вы не хотите, чтобы это было на Haswell, где это спасает верхние половины всех правил YMM, переходя в"состояние C".

очевидное решение-оставить себе царапину reg для использования для vmovq+vpblendd (вместо vpinsrq y,r,0). Это все еще 2 uops, но vpblendd не нужен порт 5 на процессорах Intel, если это имеет значение. (movq uses port 5). If you're really hard up for space, themm0..7 ' регистры MMX имеющиеся.


снижение стоимости

с вложенными циклами, мы можем разделить работу. С небольшим количеством разворачивать внутреннего петля, мы можем в основном удалить эту часть стоимости.

например, если у нас есть внутренний цикл, дающий 4 результата, мы можем использовать ваш подход к стеку грубой силы на 2 или 4 регистрах во внутреннем цикле, давая умеренные накладные расходы без фактического развертывания ("волшебная" полезная нагрузка появляется только один раз). 3 или 4 uops, выборочно без петл-снесенной цепи dep.

; on entry, rdi has the number of iterations
.outer:
    mov       r15d, 3
.inner:
; some magic happens here to calculate a result in rax

%if  AVOID_SHUFFLES
    vmovdqa   xmm3, xmm2
    vmovdqa   xmm2, xmm1
    vmovdqa   xmm1, xmm0
    vmovq     xmm0, rax
%else
    vpunpcklqdq  xmm2, xmm1, xmm2        ; { high=xmm2[0], low=xmm1[0] }
    vmovdqa   xmm1, xmm0
    vmovq     xmm0, rax
%endif

    dec   r15d
    jnz   .inner

    ;; Big block only runs once per 4 iters of the inner loop, and is only ~12 insns.
    vmovdqa  ymm15, ymm14
    vmovdqa  ymm13, ymm12
    ...

    ;; shuffle the new 4 elements into the lowest reg we read here (ymm3 or ymm4)

%if  AVOID_SHUFFLES       ; inputs are in low element of xmm0..3
    vpunpcklqdq  xmm1, xmm1, xmm0     ; don't write xmm0..2: longer false dep chain next iter.  Or break it.
    vpunpcklqdq  xmm4, xmm3, xmm2
    vinserti128  ymm4, ymm1, xmm4, 1  ; older values go in the top half
    vpxor        xmm1, xmm1, xmm1     ; shorten false-dep chains

%else                     ; inputs are in xmm2[1,0], xmm1[0], and xmm0[0]
    vpunpcklqdq  xmm3, xmm0, xmm1     ; [ 2nd-newest,  newest ]
    vinserti128  ymm3, ymm2, xmm3, 1
    vpxor        xmm2, xmm2,xmm2   ; break loop-carried dep chain for the next iter
    vpxor        xmm1, xmm1,xmm1   ; and this, which feeds into the loop-carried chain
%endif

    sub   rdi, 4
    ja   .outer

бонус: для этого требуется только AVX1 (и дешевле на AMD, сохраняя 256-битные векторы из внутреннего loop). Мы по-прежнему получаем 12 x 4 qwords хранения вместо 16 x 4. В любом случае, это было произвольное число.

общества отматывая

мы можем развернуть только внутренний цикл, например:

.top:
    vmovdqa     ymm15, ymm14
    ...
    vmovdqa     ymm3, ymm2           ; 12x movdqa
    vinserti128 ymm2, ymm0, xmm1, 1

    magic
    vmovq       xmm0, rax
    magic
    vpinsrq     xmm0, rax, 1
    magic
    vmovq       xmm1, rax
    magic
    vpinsrq     xmm1, rax, 1

    sub         rdi, 4
    ja          .top

когда мы выходим из цикла, ymm15..2 и xmm1 и 0 полный ценных данных. Если бы они были внизу, они запускались бы столько же раз, но ymm2 был бы копией xmm0 и 1. А jmp, чтобы войти в петлю, не делая vmovdqa материал на первом iter-это вариант.

в 4 раза magic, это стоит нам 6 uops для порта 5 (movq + pinsrq), 12 vmovdqa (без блока выполнения) и 1x vinserti128 (порт 5 снова). Так что это 19 uops за 4 magic, или 4.75 uops.

вы можете чередовать vmovdqa + vinsert первый magic, или просто разделить его до / после первого magic. Вы не можете clobber xmm0 до тех пор, пока vinserti128, но если у вас есть запасной целочисленный reg, вы можете отложить vmovq.

больше вложенности

другой уровень вложенности цикла,или другой отматывая, значительно уменьшило бы количество vmovdqa инструкция. Однако просто получение данных, перетасованных в правила YMM, имеет минимальную стоимость. загрузка xmm из GP regs.

AVX512 может дать нам более дешевый int - >xmm. (И это позволило бы писать всем 4 элементам YMM). Но я не вижу, как избежать необходимости развернуться. или петли гнезда, чтобы избежать касания всех регистров каждый раз.


PS:

моей первой идеей для аккумулятора shuffle было перетасовка элементов один влево. Но затем я понял, что это закончилось 5 элементами состояния, а не 4, потому что у нас было высокое и низкое в двух правилах, плюс недавно написанный xmm0. (И мог бы использовать vpalignr.)

оставляя здесь в качестве примера того, что вы можете сделать с vshufpd: перемещение от низкого к высокому в одном регистре и слияние в высокий от другого, как новый низкий.

    vshufpd   xmm2, xmm1,xmm2, 01b     ; xmm2[1]=xmm2[0], xmm2[0]=xmm1[1].  i.e. [ low(xmm2), high(xmm1) ]
    vshufpd   xmm1, xmm0,xmm1, 01b
    vmovq     xmm0, rax

AVX512: индексирование векторов в памяти

для общего случая записи в векторные регс в качестве памяти мы можем vpbroadcastq zmm0{k1}, rax и повторите для другой zmm регистрируется с другим k1 маска. Трансляции с маскированием слиянием (где маска имеет один бит) дают нам индексированное хранилище в векторные регистры, но нам нужна одна инструкция для каждого возможного назначения реестр.

создание маски:

xor      edx, edx
bts      rdx, rcx          #  rdx = 1<<(rcx&63)
kmovq     k1, rdx
kshiftrq  k2, k1, 8
kshiftrq  k3, k1, 16
...

до читать из регистра ZMM:

vpcompressq zmm0{k1}{z}, zmm1    ; zero-masking: zeros whole reg if no bits set
vpcompressq zmm0{k2},    zmm2    ; merge-masking
... repeat as many times as you have possible source regs

vmovq       rax, zmm0

(см. документы для vpcompressq: С нулевой маскировкой это нули всех элементов над тем, что он пишет)

чтобы скрыть задержку vpcompressq, вы можете сделать несколько цепочек dep в несколько векторов tmp, затем vpor xmm0, xmm0, xmm1 в конце. (Один из векторов будет равен нулю, другой будет иметь выбранный элемент.)

на SKX он имеет задержку 3c и пропускную способность 2c,согласно этому отчету instatx64.


несколько вариантов, которые вы могли бы рассмотреть:

развернуть

если вы разворачиваете свой цикл (который обязательно имеет ограниченное количество итераций, так как хранилище доступно в ymm регистры ограничены 64 qwords) у вас будет возможность использовать жестко закодированную логику для вставки результата из rax непосредственно в нужном месте, e.g,. с pinrsq или movq иногда в сочетании с перетасовкой, чтобы позволить вам получить доступ к высоким полосам. Это, вероятно, займет всего 1.25 инструкции на итерацию, намного лучше, чем 32!

Вертикальные Шпалеры

ваше текущее решение для перетасовки можно охарактеризовать как горизонтальное вращение через регистр, несущее от высокого qword ymm N в низкое qword ymm N+1. То есть соседние элементы в пределах одного регистра логически соседствуют в вашей схеме. Вместо этого вы можете сделать вертикальное вращение, где вместо элементов в данном qword Майна логически смежна к элементам в та же полоса в регистрах ymm N-1 и ymm N+1. Это позволяет избежать необходимости любого горизонтального перетасовки, и большая часть сдвига требует только одного регистра-register mov. Вам нужна только специальная обработка для первого и последнего регистров, чтобы обернуть ваши элементы в следующую полосу.

что-то вроде этого:

; shift all lanes "up"
vmovdqa ymm15, ymm3
vmovdqa  ymm3, ymm2
vmovdqa  ymm2, ymm1
vmovdqa  ymm1, ymm0

; wrap from the top register back to ymm0, shifting to the left by 1
vpermq   ymm0, ymm15, 10_01_00_11b
; store new element
vpinsrq  ymm0, rax, 0

это примерно так же просто, как вы собираетесь получить для общей стратегии "shift every element": один vmovdqa согласно ymm зарегистрироваться, плюс к дополнительным инструкциям, чтобы сделать wraparound и новую вставку элемента. Что касается векторных операций, регистр-регистровые перемещения намного быстрее, чем любой другой тип операции, поскольку они могут быть устранены (0 латентность) и могут выполняться 4 за цикл.

этот подход требует временного регистра (ymm15 в примере выше) и я не могу придумать простой способ устранить это, так что вы сможете использовать не более 15 регистров в рамках вашей "очередь."

Косвенный Переход

вы можете выполнить вычисленный косвенный прыжок на основе подсчета итераций в короткую последовательность (2-4 инструкции), которая помещает элемент в нужное место. В основном vpinsrq и в некоторых случаях некоторые дополнительные перетасовки для доступа к высокой полосе.

этот тип таблицы может быть полностью общим, т. е. разрешать записи в произвольные индексы в любом порядке, но если бы вы знали, что индексируете последовательно, как указано выше, вы могли бы упростить таблица, использующая это предположение (i.e,. вы можете обрабатывать высокую полосу, сначала записывая в низкие элементы, а затем используя vinserti128 или что-то в этом роде, чтобы переместить их обоих в высокий переулок в нужное время.

эта таблица будет предположительно mispredict первый раз. После этого он может или не может, в зависимости от шаблона и силы предсказателя непрямой ветви.