В AVX2 и ранее нет единой инструкции. (AVX512 может использовать маски в растровой форме напрямую, и имеет инструкцию по расширению масок до векторов).
В вашем случае, если вы загружаете растровое изображение из памяти, загрузка его прямо в векторные регистры для стратегии ALU должна хорошо работать даже для 4-битных масок.
Если у вас есть растровое изображение в качестве результата вычисления, оно будет в целочисленном регистре, где вы можете легко использовать его в качестве индекса LUT, так что это хороший выбор, если вы стремитесь к 64-битным элементам. В противном случае, вероятно, по-прежнему будет использоваться ALU для 32-битных элементов или меньше вместо гигантского LUT или выполнения нескольких фрагментов.
Придется дождаться регистров масок AVX-512, прежде чем станет возможным дешевое преобразование целочисленных битовых масок в векторные маски. (С kmovw k1, r/m16
, который компиляторы неявно генерируют для int => __mmask16
). Есть AVX512 insn для установки вектора из маски (VPMOVM2D zmm1, k1
, _ 10_, с другими версиями для других размеров элементов), но обычно он вам не нужен, поскольку все, что раньше использовало векторы масок, теперь использует регистры масок. Может быть, если вы хотите подсчитать элементы, удовлетворяющие какому-либо условию сравнения? (где вы должны использовать pcmpeqd
/ psubd
для генерации и накопления вектора из 0 или -1 элементов). Но скаляр popcnt
для результатов маски был бы лучше.
Но обратите внимание, что vpmovm2d
требует, чтобы маска находилась в регистре маски AVX512 k0..7
. Чтобы получить его там, потребуются дополнительные инструкции, если они не были получены из результата векторного сравнения, а инструкции, которые перемещаются в регистры масок, нуждаются в uop для порта 5 на Intel Skylake-X и аналогичных процессорах, поэтому это может быть узким местом (особенно если вы выполняете какие-либо перетасовки ). Особенно, если он начинается в памяти (загрузка растрового изображения) и вам нужен только старший бит каждого элемента, вам, вероятно, все равно будет лучше с широковещательной загрузкой + переменным сдвигом, даже если доступны 256-битные и 512-битные инструкции AVX512.
Также возможна (для результата 0/1 вместо 0 / -1) нагрузка с нулевым маскированием из константы типа _mm_maskz_mov_epi8(mask16, _mm_set1_epi8(1))
. https://godbolt.org/z/1sM8hY8Tj
Для 64-битных элементов маска имеет только 4 бита, поэтому таблица подстановки разумна. Вы можете сжать LUT, загрузив его с помощью VPMOVSXBQ ymm1, xmm2/m32
. (_mm256_cvtepi8_epi64
). Это дает вам размер LUT (1 ‹---------------- 4) = 16 * 4 байта = 64B = 1 строка кеша. К сожалению, pmovsx
является неудобно использовать как узкую нагрузку с встроенным.
Особенно, если у вас уже есть растровое изображение в целочисленном регистре (вместо памяти), vpmovsxbq
LUT должен быть отличным внутри внутреннего цикла для 64-битных элементов. Или, если пропускная способность инструкций или пропускная способность в случайном порядке является узким местом, используйте несжатый LUT. Это может позволить вам (или компилятору) использовать вектор маски в качестве операнда памяти для чего-то еще, вместо того, чтобы загружать его с помощью отдельной инструкции.
LUT для 32-битных элементов: возможно, не оптимально, но вот как это можно сделать
С 32-битными элементами 8-битная маска дает вам 256 возможных векторов, каждый из которых имеет длину 8 элементов. 256 * 8B = 2048 байт, что является довольно большим объемом кеш-памяти даже для сжатой версии (загрузка с vpmovsxbd ymm, m64
).
Чтобы обойти эту проблему, вы можете разделить LUT на 4-битные блоки. Требуется около 3 целочисленных инструкций, чтобы разделить 8-битное целое число на два 4-битных целых числа (mov/and/shr
). Затем с несжатым LUT из 128b векторов (для 32-битного размера элемента), vmovdqa
младшая половина и vinserti128
высокая половина. Вы все равно можете сжать LUT, но я бы не рекомендовал его, потому что вам понадобится vmovd
/ vpinsrd
/ vpmovsxbd
, что составляет 2 перемешивания (так что вы, вероятно, узкое место в пропускной способности uop).
Или 2x vpmovsxbd xmm, [lut + rsi*4]
+ vinserti128
наверное еще хуже на Интеле.
Альтернатива ALU: подходит для 16/32/64-битных элементов
Когда все растровое изображение умещается в каждом элементе: транслируйте его, И с маской селектора и VPCMPEQ с той же константой (которая может оставаться в регистре при многократном использовании этого в цикле).
vpbroadcastd ymm0, dword [mask] ; _mm256_set1_epi32
vpand ymm0, ymm0, setr_epi32(1<<0, 1<<1, 1<<2, 1<<3, ..., 1<<7)
vpcmpeqd ymm0, ymm0, [same constant] ; _mm256_cmpeq_epi32
; ymm0 = (mask & bit) == bit
; where bit = 1<<element_number
Маска может поступать из целочисленного регистра с помощью vmovd + vpbroadcastd, но широковещательная загрузка стоит недорого, если она уже находится в памяти, например из массива масок для применения к массиву элементов. На самом деле мы заботимся только о младших 8 битах этого двойного слова, потому что 8x 32-битных элементов = 32 байта. (например, что вы получили от vmovmaskps
). С 16-битной маской для 16x 16-битных элементов вам понадобится vpbroadcastw
. Чтобы получить такую маску в первую очередь из 16-битных целочисленных векторов, вы можете vpacksswb
два вектора вместе (что сохраняет знаковый бит каждого элемента), vpermq
, чтобы расположить элементы в последовательном порядке после внутренней упаковки, затем vpmovmskb
.
Для 8-битных элементов вам потребуется vpshufb
результат vpbroadcastd
, чтобы получить соответствующий бит в каждый байт. См. Как выполнить обратное _mm256_movemask_epi8 (VPMOVMSKB)?. Но для 16-битных и более широких элементов количество элементов ‹= ширине элемента, поэтому широковещательная загрузка делает это бесплатно. (16-битные широковещательные нагрузки действительно стоят микроплавленного ALU shuffle uop, в отличие от 32- и 64-битных широковещательных нагрузок, которые полностью обрабатываются в портах нагрузки.)
vpbroadcastd/q
даже не стоит никаких ALU упа, это делается прямо в порту загрузки. (b
и w
загружаются + перемешиваются). Даже если там ваши маски упакованы вместе (по одной на байт для 32- или 64-битных элементов), все равно может быть более эффективным vpbroadcastd
вместо vpbroadcastb
. Проверка x & mask == mask
не заботится о мусоре в старших байтах каждого элемента после широковещательной передачи. Единственное беспокойство - это разделение строк кеша / страниц.
Переменный сдвиг (дешевле на Skylake), если вам нужен только знаковый бит
Переменные смешивания и маскированные загрузки / сохранения заботятся только о знаковом бите элементов маски.
Это всего лишь 1 моп (на Skylake), если у вас есть 8-битная маска, транслируемая для элементов двойного слова.
vpbroadcastd ymm0, dword [mask]
vpsllvd ymm0, ymm0, [vec of 24, 25, 26, 27, 28, 29, 30, 31] ; high bit of each element = corresponding bit of the mask
;vpsrad ymm0, ymm0, 31 ; broadcast the sign bit of each element to the whole element
;vpsllvd + vpsrad has no advantage over vpand / vpcmpeqb, so don't use this if you need all the bits set.
vpbroadcastd
так же дешево, как загрузка из памяти (на процессорах Intel и Ryzen вообще нет ALU). (Более узкое вещание, например vpbroadcastb y,mem
, возьмите ALU shuffle uop на Intel, но, возможно, не на Ryzen.)
Сдвиг переменной немного дороже на Haswell / Broadwell (3 мупа, ограниченное количество портов исполнения), но так же дешево, как смещение немедленного подсчета на Skylake! (1 моп на порт 0 или 1.) На Ryzen они также всего 2 мупа (минимум для любой операции 256b), но имеют задержку 3c и одну на пропускную способность 4c.
См. вики-страницу по тегам x86 для получения информации о производительности. , особенно insn-таблицы Агнера Фога.
Обратите внимание, что для 64-битных элементов арифметические сдвиги вправо доступны только для 16- и 32-битных элементов. Используйте другую стратегию, если вы хотите, чтобы для всего элемента было установлено значение «все нули / все единицы» для 4 бит - ›64-битных элементов.
С внутренними характеристиками:
__m256i bitmap2vecmask(int m) {
const __m256i vshift_count = _mm256_set_epi32(24, 25, 26, 27, 28, 29, 30, 31);
__m256i bcast = _mm256_set1_epi32(m);
__m256i shifted = _mm256_sllv_epi32(bcast, vshift_count); // high bit of each element = corresponding bit of the mask
return shifted;
// use _mm256_and and _mm256_cmpeq if you need all bits set.
//return _mm256_srai_epi32(shifted, 31); // broadcast the sign bit to the whole element
}
Внутри цикла LUT может стоить занимаемого места в кэше, в зависимости от сочетания инструкций в цикле. Особенно для 64-битного размера элемента, где не так много места в кеше, но, возможно, даже для 32-битного.
Другой вариант, вместо сдвига переменной, - использовать BMI2 для распаковки каждого бита в байт с этим элементом маски в старшем бите, затем vpmovsx
:
; 8bit mask bitmap in eax, constant in rdi
pdep rax, rax, rdi ; rdi = 0b1000000010000000... repeating
vmovq xmm0, rax
vpmovsxbd ymm0, xmm0 ; each element = 0xffffff80 or 0
; optional
;vpsrad ymm0, ymm0, 8 ; arithmetic shift to get -1 or 0
Если у вас уже есть маски в целочисленном регистре (где вам все равно придется vmovq
/ vpbroadcastd
отдельно), то этот способ, вероятно, лучше даже для Skylake, где сдвиги переменного подсчета дешевы.
Если ваши маски начинаются в памяти, другой метод ALU (vpbroadcastd
непосредственно в вектор), вероятно, лучше, потому что широковещательные загрузки настолько дешевы.
Обратите внимание, что pdep
- это 6 зависимых мопов на Ryzen (задержка 18c, пропускная способность 18c), поэтому этот метод ужасен для Ryzen, даже если ваши маски начинаются с целочисленных регистров.
(Будущие читатели, не стесняйтесь редактировать во встроенной версии этого. Легче писать asm, потому что он намного меньше печатает, а мнемонику asm легче читать (без глупого _mm256_
беспорядка повсюду).)
person
Peter Cordes
schedule
08.04.2016