Есть ли обратная инструкция к инструкции movemask в intel avx2?
Инструкция (-и) movemask принимают __m256i и возвращают int32, где каждый бит (либо первые 4, 8 или все 32 бита в зависимости от типа входного векторного элемента) является старшим значащим битом соответствующего векторного элемента.
Я хотел бы сделать обратное: взять 32 (где только 4, 8 или 32 младшие значащие биты имеют значение), и получить __m256i, где самый старший бит каждого блока размером int8, int32 или int64 установлен в исходное значение немного.
По сути, я хочу перейти от сжатой битовой маски к той, которая может использоваться в качестве маски другими инструкциями AVX2 (такими как maskstore, maskload, mask_gather).
Я не мог быстро найти инструкцию, которая делает это, поэтому я спрашиваю здесь. Если нет такой инструкции с такой функциональностью, есть ли хитрый способ взломать, который достигает этого за очень мало инструкций?
Мой текущий метод заключается в использовании таблицы поиска из 256 элементов. Я хочу использовать эту операцию в цикле, где больше ничего не происходит, чтобы ускорить ее. Обратите внимание, меня не слишком интересуют длинные последовательности из нескольких команд или маленькие циклы, которые реализуют эту операцию.
1 ответ
В AVX2 и более ранних версиях нет ни одной инструкции.
- 4 бита -> 4 слова в регистре YMM: ответ: LUT это хорошо, ALU тоже хорошо
- 8 бит -> 8 слов в регистре YMM: этот ответ: ALU - это хорошо
- 16 бит -> 16 слов: ответ с
vpbroadcastw
/vpand
/vpcmpeqw
- 32 бита -> 32 байта:
Как выполнить инверсию _mm256_movemask_epi8 (VPMOVMSKB)?
Также самый быстрый способ распаковать 32 бита в 32-байтовый вектор SIMD.
Если вы загружаете растровое изображение из памяти, загрузка его прямо в векторные регистры для стратегии ALU должна работать хорошо.
Если у вас есть растровое изображение в качестве результата вычисления, то оно будет в целочисленном регистре, где вы можете легко использовать его в качестве индекса LUT, так что это хороший выбор, если вы стремитесь к 64-битным элементам. В противном случае, вероятно, все равно будет идти ALU для 32-битных элементов или меньше, вместо гигантского LUT или выполнения нескольких кусков.
Нам придется подождать регистры масок AVX-512, прежде чем станет возможным дешевое преобразование из целочисленных битовых масок в векторные маски. (С kmovw k1, r/m16
, которые компиляторы генерируют неявно для int => __mmask16
). Есть AVX512 insn, чтобы установить вектор из маски (VPMOVM2D zmm1, k1
, _mm512_movm_epi8/16/32/64
с другими версиями для элементов разных размеров), но вам, как правило, это не нужно, поскольку все, что раньше использовало векторы маски, теперь использует регистры маски. Может быть, если вы хотите посчитать элементы, которые удовлетворяют условию сравнения? (где бы вы использовали pcmpeqd
/ psubd
генерировать и накапливать вектор из 0 или -1 элементов). Но скаляр popcnt
на маску результаты будет лучше ставить.
Для 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).
Или 2х vpmovsxbd xmm, [lut + rsi*4]
+ vinserti128
Вероятно, еще хуже на Intel.
Альтернатива ALU: хорошо для 16/32/64-битных элементов
Когда все растровое изображение помещается в каждый элемент, передайте его, И с маской селектора, и VPCMPEQ для одной и той же константы (которая может оставаться в регистре при многократном использовании этого в цикле).
vpbroadcastd ymm0, dword [mask]
vpand ymm0, ymm0, [vec of 1<<0, 1<<1, 1<<2, 1<<3, ...]
vpcmpeqd ymm0, ymm0, [same constant]
; ymm0 = (mask & bit) == bit
; where bit = 1<<element_number
(Маска может прийти из целочисленного регистра с помощью vmovd + vpbroadcastd, но широковещательная загрузка
Для 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
check не заботится о мусоре в старших байтах каждого элемента после трансляции. Единственное беспокойство - это разделение строки кэша / страницы.
Переменное смещение (дешевле на Skylake), если вам нужен только бит знака
Переменные смеси и маскированные загрузки / хранилища заботятся только о знаковых битах элементов маски.
Это только 1 моп (на Skylake), когда у вас есть 8-битная маска, передаваемая элементам dword.
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
такая же дешевая, как загрузка из памяти (вообще нет ALU uop на процессорах Intel и Ryzen). (Узкие трансляции, как 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-битных.
Другой вариант, вместо переменной shift, состоит в использовании 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_
беспорядок везде).)