есть ли инструкция, обратная инструкции movemask в Intel avx2?

Инструкции movemask принимают __m256i и возвращают int32, где каждый бит (первые 4, 8 или все 32 бита в зависимости от типа входного элемента вектора) является самым старшим битом соответствующего элемента вектора.

Я хотел бы сделать обратное: взять 32 (где значимы только 4, 8 или 32 наименее значимых бита) и получить __m256i, в котором самый значимый бит каждого блока размером int8, int32 или int64 установлен на исходный немного.

По сути, я хочу перейти от сжатой битовой маски к той, которая может использоваться в качестве маски другими инструкциями AVX2 (такими как maskstore, maskload, mask_gather).

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

Мой текущий метод - использовать поисковую таблицу из 256 элементов. Я хочу использовать эту операцию в цикле, где больше ничего не происходит, чтобы ускорить ее. Обратите внимание: меня не слишком интересуют длинные последовательности из нескольких инструкций или маленькие циклы, реализующие эту операцию.


person orm    schedule 07.04.2016    source источник
comment
Возможный дубликат Как выполнить обратное _mm256_movemask_epi8 (VPMOVMSKB )?   -  person Peter Cordes    schedule 08.04.2016
comment
Много хороших ответов на этот потенциальный дубликат, но в основном они рассматривают случай 8-битного элемента. Мой ответ здесь действительно охватывал только 32-битный элемент. (потому что переменные сдвиги не существуют для более узких элементов)   -  person Peter Cordes    schedule 08.04.2016


Ответы (1)


В 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
comment
Еще хуже, если ваши маски начинаются в памяти, поскольку широковещательная загрузка в вектор обходится очень дешево. - не могли бы вы прояснить это? Что хуже и что лучше? Мои маски начинаются в памяти (а я использую Ryzen), так что мне использовать? - person Serge Rogatch; 31.08.2017
comment
@SergeRogatch: Тогда оба фактора в пользу метода переменного сдвига. (Или, может быть, сжатый LUT, поскольку у вас есть 64-битные элементы.) - person Peter Cordes; 31.08.2017
comment
@PeterCordes: ALU alternative: good for 16/32/64-bit elements - Я не понимаю, как это работает для 16 короткометражек. Я что-то упускаю? - person Denis Yaroshevskiy; 08.05.2020
comment
@DenisYaroshevskiy: Я не уверен, какая проблема, по вашему мнению, может быть, поскольку вы не упомянули о ней. _mm256_set1_epi16 повторяет 16-битную маску 16 раз. Векторная константа _mm256_setr_epi16(1<<0, 1<<1, ..., 1<<15) может соответствовать одному биту в каждом элементе, поскольку ширина элемента не меньше ширины маски. vpbroadcastw, vpand и vpcmpeqw все существуют в AVX2. - person Peter Cordes; 08.05.2020
comment
@PeterCordes, извините - я имел в виду, что вам нужны две разные части маски для двух частей. Старшие 128 бит будут иметь старшие 16 бит, а младшие 128 бит - младшие 16 бит. Другой способ сказать, что вся 32-битная маска не поместится в epi16. Я могу чего-то не понять. - person Denis Yaroshevskiy; 08.05.2020
comment
@DenisYaroshevskiy: Какая 32-битная маска? 32-байтовый вектор YMM содержит 16 16-битных элементов, поэтому вам понадобится только 16-битная маска. Если у вас 32 бита маски, то каждую 16-битную половину можно развернуть отдельно в отдельные __m256i переменные. Для 32-битных элементов я использовал загрузку vpbroadcastd, потому что она дешевле, чем vpbroadcastb, и нам нужны только 8 битов маски внизу каждого элемента вектора двойного слова. - person Peter Cordes; 08.05.2020
comment
@PeterCordes - movemask вернет 32-битную маску, верно? Каждый второй бит дублируется, но с этим нужно бороться. - person Denis Yaroshevskiy; 08.05.2020
comment
@DenisYaroshevskiy: Я не о том говорю. Мой ответ - 1 бит на 2-байтовый элемент, где вы сделали упаковку своей битовой маски. например с vpacksswb + vpermq перед vpmovmskb, чтобы сузить элементы вектора, сохраняя бит знака. 32/64-битные элементы проще, просто используйте vmovmskps/d. Если вы берете результат _mm256_movemask_epi8 напрямую, это все равно байтовая маска для 8-битных элементов, и вам придется распаковать ее как таковую. (Возможно, возможны некоторые оптимизации, если вы знаете о избыточности). Я подумаю об обновлении этого ответа на случай, если у кого-то возникнет такое же недоразумение. - person Peter Cordes; 08.05.2020