Как выполнить инверсию _mm256_movemask_epi8 (VPMOVMSKB)?

Внутренняя:

int mask = _mm256_movemask_epi8(__m256i s1)

создает маску с ее 32 битами, соответствующими старшему биту каждого байта s1. После манипулирования маской с помощью битовых операций (например, BMI2) я хотел бы выполнить инверсию _mm256_movemask_epi8, то есть создать вектор __m256i со старшим битом каждого байта, содержащим соответствующий бит uint32_t mask.

Как лучше всего это сделать?

Изменить: мне нужно выполнить обратное, потому что внутренняя _mm256_blendv_epi8 принимает только маску типа __m256i вместо uint32_t. Таким образом, в результирующей маске __m256i я могу игнорировать биты, отличные от MSB каждого байта.


person Satya Arjunan    schedule 07.02.2014    source источник
comment
с AVX512 вы можете использовать _mm256_mask_blend_epi8(__mmask32 k, __m256i a, __m256i b), используя целое число в качестве маски   -  person technosaurus    schedule 29.08.2015
comment
См. Также мой ответ на возможный повторяющийся вопрос. Используйте сдвиг переменной vpsllvd, чтобы поместить разные биты маски в бит знака каждого элемента. Это отлично подходит для элемента размером 32b, но не для 8b.   -  person Peter Cordes    schedule 08.04.2016


Ответы (5)


Вот альтернатива инструкциям LUT или pdep, которые могут быть более эффективными:

  1. Скопируйте свою 32-битную маску как в младшие байты некоторого ymm регистра, так и в байты 16..19 того же регистра. Вы можете использовать временный массив и _mm256_load_si256. Или вы можете переместить одну копию 32-битной маски в младшие байты какого-либо ymm регистра, а затем передать ее с помощью VPBROADCASTD (_mm_broadcastd_epi32) или других инструкций широковещательной передачи / перемешивания.
  2. Переупорядочьте байты регистра так, чтобы младшие 8 байтов (каждый) содержали младшие 8 бит вашей маски, следующие 8 байтов - следующие 8 бит и т. Д. Это можно сделать с помощью VPSHUFB (_mm256_shuffle_epi8) с управляющим регистром, содержащим «0» в младших 8 байтах, 1 'в следующих 8 байтах и ​​т. Д.
  3. Выберите соответствующий бит для каждого байта с помощью VPOR (_mm256_or_si256) или VPAND (_mm256_and_si256).
  4. Установите MSB соответствующих байтов с помощью VPCMPEQB (_mm256_cmpeq_epi8). Сравните каждый байт с 0xFF. Если вы хотите переключать каждый бит маски, используйте VPAND на предыдущем шаге и сравните с нулем.

Дополнительная гибкость этого подхода заключается в том, что вы можете выбрать другой управляющий регистр для шага №2 и другую маску для шага №3, чтобы перемешать биты вашей битовой маски (например, вы можете скопировать эту маску в регистр ymm в обратном порядке).

person Evgeny Kluev    schedule 07.02.2014
comment
Просто используйте _mm256_set1_epi32 и позвольте компилятору выполнить широковещательную загрузку с vpbroadcastd ymm, [mem], если он захочет. - person Peter Cordes; 08.04.2016
comment
После перемешивания используйте VPAND и VPCMPEQB для реализации bitmap & (1<<bit) == (1<<bit). Вам нужна только одна векторная константа. - person Peter Cordes; 09.11.2017
comment
Если вы хотите 0/1 вместо 0/0xff, используйте _mm256_min_epu8(and_result, _mm256_set1_epi8(1)) вместо cmpeq против маски AND. Элементы с ненулевым байтом будут иметь минимум 1 по сравнению с min(0,1) = 0. (этот трюк из Как эффективно преобразовать 8-битное растровое изображение в массив целых чисел 0/1 с помощью SIMD x86) - person Peter Cordes; 31.08.2018

Я реализовал три вышеуказанных подхода на машине Haswell. Подход Евгения Клюева самый быстрый (1,07 с), за ним следуют Джейсон Р. (1,97 с) и Пол Р. (2,44 с). Приведенный ниже код был скомпилирован с флагами оптимизации -march = core-avx2 -O3.

#include <immintrin.h>
#include <boost/date_time/posix_time/posix_time.hpp>

//t_icc = 1.07 s
//t_g++ = 1.09 s
__m256i get_mask3(const uint32_t mask) {
  __m256i vmask(_mm256_set1_epi32(mask));
  const __m256i shuffle(_mm256_setr_epi64x(0x0000000000000000,
      0x0101010101010101, 0x0202020202020202, 0x0303030303030303));
  vmask = _mm256_shuffle_epi8(vmask, shuffle);
  const __m256i bit_mask(_mm256_set1_epi64x(0x7fbfdfeff7fbfdfe));
  vmask = _mm256_or_si256(vmask, bit_mask);
  return _mm256_cmpeq_epi8(vmask, _mm256_set1_epi64x(-1));
}

//t_icc = 1.97 s
//t_g++ = 1.97 s
__m256i get_mask2(const uint32_t mask) {
  __m256i vmask(_mm256_set1_epi32(mask));
  const __m256i shift(_mm256_set_epi32(7, 6, 5, 4, 3, 2, 1, 0));
  vmask = _mm256_sllv_epi32(vmask, shift);
  const __m256i shuffle(_mm256_setr_epi64x(0x0105090d0004080c,
      0x03070b0f02060a0e, 0x0105090d0004080c, 0x03070b0f02060a0e));
  vmask = _mm256_shuffle_epi8(vmask, shuffle);
  const __m256i perm(_mm256_setr_epi64x(0x0000000000000004, 0x0000000100000005,
      0x0000000200000006, 0x0000000300000007));
  return _mm256_permutevar8x32_epi32(vmask, perm);
}

//t_icc = 2.44 s
//t_g++ = 2.45 s
__m256i get_mask1(uint32_t mask) {
  const uint64_t pmask = 0x8080808080808080ULL; // bit unpacking mask for PDEP
  uint64_t amask0, amask1, amask2, amask3; 
  amask0 = _pdep_u64(mask, pmask);
  mask >>= 8;
  amask1 = _pdep_u64(mask, pmask);
  mask >>= 8;
  amask2 = _pdep_u64(mask, pmask);
  mask >>= 8;
  amask3 = _pdep_u64(mask, pmask);
  return _mm256_set_epi64x(amask3, amask2, amask1, amask0);
}

int main() {
  __m256i mask;
  boost::posix_time::ptime start(
      boost::posix_time::microsec_clock::universal_time()); 
  for(unsigned i(0); i != 1000000000; ++i)
    { 
      mask = _mm256_xor_si256(mask, get_mask3(i));
    }
  boost::posix_time::ptime end(
      boost::posix_time::microsec_clock::universal_time());
  std::cout << "duration:" << (end-start) << 
    " mask:" << _mm256_movemask_epi8(mask) << std::endl;
  return 0;
}
person Satya Arjunan    schedule 10.02.2014
comment
+1 за выполнение всех трех предложений и хорошее резюме результатов! Ради интереса, какой компилятор вы использовали? - person Paul R; 10.02.2014
comment
Спасибо! Я использовал и icc, и g ++. Я обновил тайминги с помощью флагов оптимизации. - person Satya Arjunan; 11.02.2014
comment
FWIW Я провел несколько тестов с помощью clang и получил аналогичные результаты. - person Paul R; 11.02.2014
comment
лязгать результаты: get_mask3: 0.9968 ns, get_mask2: 1.7413 ns, get_mask1: (check = 0) 2.291 ns - person Paul R; 11.02.2014

Мой первоначальный подход к этому был похож на подход @Jason R, потому что так работают «обычные» операции, но большинство из этих операций заботятся только о старшем бите, игнорируя все остальные биты. Как только я понял это, серия функций стала наиболее разумной. Вам нужно будет включить -mavx512vl и -mavx512bw (gcc)

Чтобы получить вектор с наивысшим битом каждого байта, установленным в соответствии с маской:

/* convert 16 bit mask to __m128i control byte mask */
_mm_maskz_broadcastb_epi8((__mmask16)mask,_mm_set1_epi32(~0))
/* convert 32 bit mask to __m256i control byte mask */
_mm256_maskz_broadcastb_epi8((__mmask32)mask,_mm_set1_epi32(~0))
/* convert 64 bit mask to __m512i control byte mask */
_mm512_maskz_broadcastb_epi8((__mmask64)mask,_mm_set1_epi32(~0))

Чтобы получить вектор с наивысшим битом каждого слова, установленным в соответствии с маской:

/* convert 8 bit mask to __m128i control word mask */
_mm_maskz_broadcastw_epi16((__mmask8)mask,_mm_set1_epi32(~0))
/* convert 16 bit mask to __m256i control word mask */
_mm256_maskz_broadcastw_epi16((__mmask16)mask,_mm_set1_epi32(~0))
/* convert 32 bit mask to __m512i control word mask */
_mm512_maskz_broadcastw_epi16((__mmask32)mask,_mm_set1_epi32(~0))

Чтобы получить вектор с наивысшим битом каждого двойного слова, установленным в соответствии с маской:

/* convert 8 bit mask to __m256i control mask */
_mm256_maskz_broadcastd_epi32((__mmask8)mask,_mm_set1_epi32(~0))
/* convert 16 bit mask to __m512i control mask */
_mm512_maskz_broadcastd_epi32((__mmask16)mask,_mm_set1_epi32(~0))

Чтобы получить вектор с наивысшим битом каждого четверного слова, установленным в соответствии с маской:

/* convert 8 bit mask to __m512i control mask */
_mm512_maskz_broadcastq_epi64((__mmask8)mask,_mm_set1_epi32(~0))

Один конкретный вопрос для этого вопроса: _mm256_maskz_broadcastb_epi8((__mmask32)mask,_mm_set1_epi32(~0)), но я включаю другие для справки / сравнения.

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

Еще одно замечание: каждый _mm_set1_epi32(~0) может / должен быть преобразован в константу (вручную или компилятором), поэтому он должен скомпилироваться всего за одну довольно быструю операцию, хотя при тестировании это может быть немного быстрее, чем в реальной жизни, поскольку константа, вероятно, будет оставаться в реестре. Затем они преобразуются в инструкции VPMOVM2 {b, w, d, q}

Изменить: если ваш компилятор не поддерживает AVX512, версия встроенной сборки должна выглядеть так:

inline __m256i dmask2epi8(__mmask32 mask){
  __m256i ret;
  __asm("vpmovm2b   %1, %0":"=x"(ret):"k"(mask):);
  return ret;
}

Остальные инструкции аналогичны.

person technosaurus    schedule 27.08.2015
comment
Если вы хотите 0 / -1, используйте _mm256_movm_epi8(mask), а не широковещательную передачу с нулевой маской. Другой вариант для значения, отличного от -1, - _mm256_maskz_mov_epi8(mask32, _mm256_set1_epi8(1)). Если бы не vpmovm2b, широковещательная передача была бы интересна, потому что создание 128-битных универсальных единиц немного дешевле (vpcmpeqd same,same имеет специальный регистр как dep-break), чем 512-битные (vpternlogd z,z,z, 0xff), но широковещательные передачи - это случайные числа, которые могут выполняться только на порте. 5. См. Также раздел AVX-512 в Преобразование 16-битной маски в 16-байтовую маску (которая в основном требует 0/1, а не обычного 0 / -1) - person Peter Cordes; 01.05.2021

Вот еще одна реализация, которая может работать на AVX2, поскольку у вас есть этот тег в вашем вопросе (он не тестировался, так как у меня нет машины Haswell). Он похож на ответ Евгения Клюева, но может потребовать меньше инструкций. Однако для этого требуются две постоянные __m256i маски. Если вы делаете это много раз в цикле, то накладные расходы на предварительную настройку этих констант могут быть незначительными.

  • Возьмите свою 32-битную маску и передайте ее во все 8 слотов ymm регистра, используя _mm_broadcastd_epi32().

  • Создайте __m256i, содержащий 8 32-битных целых чисел со значениями [0, 1, 2, 3, 4, 5, 6, 7] (от наименее значимого до наиболее значимого элемента).

  • Используйте эту постоянную маску, чтобы повернуть каждое из 32-битных целых чисел в вашем регистре ymm влево на другую величину, используя _mm256_sllv_epi32().

  • Теперь, если мы рассмотрим регистр ymm как содержащий 8-битные целые числа и посмотрим на их MSB, то теперь регистр будет содержать MSB для байтовых индексов [7, 15, 23, 31, 6, 14, 22, 30, 5, 13, 21, 29, 4, 12, 20, 28, 3, 11, 19, 27, 2, 10, 18, 26, 1, 9, 17, 25, 0, 8, 16, 24] (от наименее значимого до наиболее значимого элемента).

  • Используйте побитовое И против постоянной маски [0x80, 0x80, 0x80, ...], чтобы изолировать MSB от каждого байта.

  • Используйте последовательность перестановок и / или перестановок, чтобы вернуть элементы в нужном вам порядке. К сожалению, для 8-битных целых чисел нет взаимозависимой перестановки, как это есть для значений с плавающей запятой в AVX2.

person Jason R    schedule 07.02.2014

Единственный достаточно эффективный способ, который я могу придумать, - это 8-битный LUT: выполнить поиск 4 x 8 бит, а затем загрузить результаты в вектор, например

static const uint64_t LUT[256] = { 0x0000000000000000ULL,
                                   ...
                                   0xffffffffffffffffULL };

uint64_t amask[4] __attribute__ ((aligned(32)));

uint32_t mask;
__m256i vmask;

amask[0] = LUT[mask & 0xff];
amask[1] = LUT[(mask >> 8) & 0xff];
amask[2] = LUT[(mask >> 16) & 0xff];
amask[3] = LUT[mask >> 24];
vmask = _mm256_load_si256((__m256i *)amask);

В качестве альтернативы вы можете использовать регистры вместо временного массива и посмотреть, может ли ваш компилятор сделать что-то более эффективное, не связанное с использованием памяти:

static const uint64_t LUT[256] = { 0x0000000000000000ULL,
                                   ...
                                   0xffffffffffffffffULL };

uint64_t amask0, amask1, amask2, amask3;

uint32_t mask;
__m256i vmask;

amask0 = LUT[mask & 0xff];
amask1 = LUT[(mask >> 8) & 0xff];
amask2 = LUT[(mask >> 16) & 0xff];
amask3 = LUT[mask >> 24];
vmask = _mm256_set_epi64x(amask3, amask2, amask1, amask0);

Запоздалая мысль: интересной задачей может быть использование, например, Инструкции Haswell BMI для выполнения эквивалента 8-> 64-битной операции LUT и тем самым избавления от LUT. Похоже, вы могли бы использовать для этого PDEP, например

const uint64_t pmask = 0x8080808080808080ULL; // bit unpacking mask for PDEP

uint64_t amask0, amask1, amask2, amask3;

uint32_t mask;
__m256i vmask;

amask0 = _pdep_u64(mask, pmask); mask >>= 8;
amask1 = _pdep_u64(mask, pmask); mask >>= 8;
amask2 = _pdep_u64(mask, pmask); mask >>= 8;
amask3 = _pdep_u64(mask, pmask);
vmask = _mm256_set_epi64x(amask3, amask2, amask1, amask0);
person Paul R    schedule 07.02.2014
comment
Да, я хочу по возможности избегать LUT, они очень дороги по сравнению с операциями на основе регистров, которые я выполняю. - person Satya Arjunan; 07.02.2014