Использование регистров 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, the
mm0..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 первый раз. После этого он может или не может, в зависимости от шаблона и силы предсказателя непрямой ветви.