Как векторизовать data_i16 [от 0 до 15]? - PullRequest
2 голосов
/ 26 апреля 2020

Я на сайте Intel Intrinsi c и не могу понять, какую комбинацию инструкций я хочу. То, что я хотел бы сделать, это

result = high_table[i8>>4] & low_table[i8&15]

, где обе таблицы 16 бит (или больше). shuffle выглядит как то, что я хочу (_mm_shuffle_epi8), однако получение 8-битного значения не работает для меня. Похоже, что 16-битной версии не существует, и для не байтовой версии второй параметр необходим в качестве непосредственного значения.

Как мне это реализовать? Должен ли я вызывать _mm_shuffle_epi8 дважды для каждой таблицы, приводить ее к 16 битам и сдвигать значение на 8? Если да, на какую инструкцию приведения и сдвига я хочу посмотреть?

1 Ответ

4 голосов
/ 26 апреля 2020

Чтобы разделить ваши входящие индексы на два вектора полубайтов, вам понадобится обычный сдвиг битов и AND. SSE не имеет 8-битных сдвигов, поэтому вы должны эмулировать с более широким сдвигом и AND, чтобы скрыть биты, сдвинутые в верхнюю часть ваших байтов. (Потому что, к сожалению, для этого варианта использования _mm_shuffle_epi8 не игнорирует старшие биты. Если установлен верхний бит селектора, это нули, которые выводят элемент.)

Вы определенно делаете , а не хотите расширить входящий вектор i8 до 16-битных элементов; это не может быть использовано с _mm_shuffle_epi8.


AVX2 имеет vpermd: выбрать слова из вектора из 8x 32-битных элементов. (только 3-битные индексы, так что это не подходит для вашего варианта использования, если только ваши клевы не только 0,7). AVX512BW имеет более широкие тасовки, включая vpermi2w для индексации в таблице сцепления двух векторов или просто vpermw для индексирования слов.

Но для 128-битных векторов только с SSSE3, да pshufb (_mm_shuffle_epi8) - это путь к go. Вам понадобятся два отдельных вектора для high_table, один для старшего байта и один для младшего байта каждой записи слова. И еще два вектора для половинок low_table.

Используйте _mm_unpacklo_epi8 и _mm_unpackhi_epi8 для перемежения младших 8 байтов двух векторов или старших 8 байтов двух векторов . Это даст вам 16-битные результаты LUT, которые вы хотите, с верхней половиной каждого слова, взятой из вектора верхней половины.

т.е. вы создаете 16-битную LUT из двух 8-битных LUT с этим чередованием И вы повторяете процесс дважды для двух разных LUT.


Код будет выглядеть примерно так:

// UNTESTED, haven't tried even compiling this.

// produces 2 output vectors, you might want to just put this in a loop instead of making a helper function for 1 vector.
// so I'll omit actually returning them.
void foo(__m128i indices)
{
   // these optimize away, only used at compile time for the vector initializers
   static const uint16_t high_table[16] = {...},
   static const uint16_t low_table[16] =  {...};

   // each LUT needs a separate vector of high-byte and low-byte parts
   // don't use SIMD intrinsics to load from the uint16_t tables and deinterleave at runtime, just get the same 16x 2 x 2 bytes of data into vector constants at compile time.
   __m128i high_LUT_lobyte = _mm_setr_epi8(high_table[0]&0xff, high_table[1]&0xff, high_table[2]&0xff, ... );
   __m128i high_LUT_hibyte = _mm_setr_epi8(high_table[0]>>8, high_table[1]>>8, high_table[2]>>8, ... );

   __m128i low_LUT_lobyte = _mm_setr_epi8(low_table[0]&0xff, low_table[1]&0xff, low_table[2]&0xff, ... );
   __m128i low_LUT_hibyte = _mm_setr_epi8(low_table[0]>>8, low_table[1]>>8, low_table[2]>>8, ... );


// split the input indexes: emulate byte shift with wider shift + AND
    __m128i lo_idx = _mm_and_si128(indices, _mm_set1_epi8(0x0f));
    __m128i hi_idx = _mm_and_si128(_mm_srli_epi32(indices, 4), _mm_set1_epi8(0x0f));

    __m128i lolo = _mm_shuffle_epi8(low_LUT_lobyte, lo_idx);
    __m128i lohi = _mm_shuffle_epi8(low_LUT_hibyte, lo_idx);

    __m128i hilo = _mm_shuffle_epi8(high_LUT_lobyte, hi_idx);
    __m128i hihi = _mm_shuffle_epi8(high_LUT_hibyte, hi_idx);

   // interleave results of LUT lookups into vectors 16-bit elements
    __m128i low_result_first  = _mm_unpacklo_epi8(lolo, lohi);
    __m128i low_result_second = _mm_unpackhi_epi8(lolo, lohi);
    __m128i high_result_first  = _mm_unpacklo_epi8(hilo, hihi);
    __m128i high_result_second = _mm_unpackhi_epi8(hilo, hihi);

    // first 8x 16-bit high_table[i8>>4] & low_table[i8&15] results
    __m128i and_first = _mm_and_si128(low_result_first, high_result_first);
    // second 8x 16-bit high_table[i8>>4] & low_table[i8&15] results
    __m128i and_second = _mm_and_si128(low_result_second, high_result_second);

    // TOOD: do something with the results.
}

Вы могли бы И до чередования, высокие половины против высоких полов и низкие против низких. Это может быть несколько лучше для параллелизма на уровне команд, позволяя выполнению AND перекрываться с тасовками. (Intel Haswell через Skylake имеет только 1 / тактовую пропускную способность для перемешивания.)

Выбор имен переменных - это борьба с подобными вещами. Некоторые люди просто сдаются и используют бессмысленные имена для некоторых промежуточных шагов.

...