Эффективная генерация маски тасования sse для байтовых элементов с левой упаковкой

Какой эффективный способ оптимизировать следующий код с помощью sse?

uint16_t change1= ... ;
uint8_t* pSrc   = ... ;
uint8_t* pDest  = ... ;

if(change1 & 0x0001) *pDest++ = pSrc[0];
if(change1 & 0x0002) *pDest++ = pSrc[1];
if(change1 & 0x0004) *pDest++ = pSrc[2];
if(change1 & 0x0008) *pDest++ = pSrc[3];

if(change1 & 0x0010) *pDest++ = pSrc[4];
if(change1 & 0x0020) *pDest++ = pSrc[5];
if(change1 & 0x0040) *pDest++ = pSrc[6];
if(change1 & 0x0080) *pDest++ = pSrc[7];

if(change1 & 0x0100) *pDest++ = pSrc[8];
if(change1 & 0x0200) *pDest++ = pSrc[9];
if(change1 & 0x0400) *pDest++ = pSrc[10];
if(change1 & 0x0800) *pDest++ = pSrc[11];

if(change1 & 0x1000) *pDest++ = pSrc[12];
if(change1 & 0x2000) *pDest++ = pSrc[13];
if(change1 & 0x4000) *pDest++ = pSrc[14];
if(change1 & 0x8000) *pDest++ = pSrc[15];

До сих пор я использую для этого довольно большую таблицу поиска, но я действительно хочу избавиться от нее:

SSE3Shuffle::Entry& e0 = SSE3Shuffle::g_Shuffle.m_Entries[change1];
_mm_storeu_si128((__m128i*)pDest, _mm_shuffle_epi8(*(__m128i*)pSrc, e0.mask));
pDest += e0.offset;

person Olaf Reusch    schedule 04.08.2017    source источник
comment
Существуют ли вообще какие-либо ограничения на содержимое change1 или это может быть любой 16-битный шаблон?   -  person Paul R    schedule 04.08.2017
comment
используются все биты, отображаются все битовые комбинации.   -  person Olaf Reusch    schedule 04.08.2017
comment
С помощью маски перемещения && pext вы можете разрезать все биты от __m128i до 8 регистров, но я не знаю, как их деинтерливировать...   -  person Aki Suihkonen    schedule 04.08.2017
comment
Можно сделать такое сжатие так же, как эмулируется pext, но это ужасно.   -  person harold    schedule 04.08.2017
comment
Связанный: маска BMI2 -генерация для AVX2 левая упаковка 32-битных или 64-битных элементов. Неприменимо напрямую, потому что он не может легко обрабатывать более мелкие элементы.   -  person Peter Cordes    schedule 08.08.2017
comment
Точно так же этот ответ может вас заинтересовать. Он показывает, как сгенерировать маску тасования _mm_shuffle_epi8 для элементов размером 16x8 бит с помощью инструкции pext. В вашем случае вы можете опустить часть «0xFF в нежелательных позициях». Хотя вам может понадобиться файл popcnt.   -  person wim    schedule 08.08.2017


Ответы (2)


Предполагая:

change1 = _mm_movemask_epi8(bytemask);
offset = popcnt(change1);

На больших буферах использование двух перетасовок и таблицы размером 1 КиБ всего на ~10% медленнее, чем использование одной перетасовки и таблицы размером 1 МБ. Мои попытки сгенерировать маску тасования с помощью сумм префиксов и перестановки битов примерно вдвое медленнее, чем методы, основанные на таблице (решения с использованием pext/pdep не изучались).

Уменьшение размера таблицы: используйте два поиска в таблице размером 2 КиБ вместо одного поиска в таблице размером 1 МиБ. Всегда сохраняйте самый верхний байт - если этот байт нужно отбросить, то не имеет значения, какой байт находится в этой позиции (до 7-битных индексов или таблицы размером 1 КиБ). Еще больше уменьшите возможные комбинации, вручную упаковав два байта в каждую 16-битную дорожку (до 216-байтовой таблицы).

В следующем примере пробелы удаляются из текста с помощью SSE4.1. Если доступен только SSSE3, можно эмулировать blendv. 64-битные половины повторно объединяются путем перекрывающихся записей в память, но они могут быть повторно объединены в регистре xmm (как показано в примере AVX2).

#include <stdint.h>
#include <smmintrin.h> // SSE4.1

size_t despacer (void* dst_void, void* src_void, size_t length)
{
    uint8_t* src = (uint8_t*)src_void;
    uint8_t* dst = (uint8_t*)dst_void;

    if (length >= 16) {
        // table of control characters (space, tab, newline, carriage return)
        const __m128i lut_cntrl = _mm_setr_epi8(' ', 0, 0, 0, 0, 0, 0, 0, 0, '\t', '\n', 0, 0, '\r', 0, 0);

        // bits[4:0] = index -> ((trit_d * 0) + (trit_c * 9) + (trit_b * 3) + (trit_a * 1))
        // bits[15:7] = popcnt
        const __m128i sadmask = _mm_set1_epi64x(0x8080898983838181);

        // adding 8 to each shuffle index is cheaper than extracting the high qword
        const __m128i offset = _mm_cvtsi64_si128(0x0808080808080808);

        // shuffle control indices
        static const uint64_t table[27] = {
            0x0000000000000706, 0x0000000000070600, 0x0000000007060100, 0x0000000000070602,
            0x0000000007060200, 0x0000000706020100, 0x0000000007060302, 0x0000000706030200,
            0x0000070603020100, 0x0000000000070604, 0x0000000007060400, 0x0000000706040100,
            0x0000000007060402, 0x0000000706040200, 0x0000070604020100, 0x0000000706040302,
            0x0000070604030200, 0x0007060403020100, 0x0000000007060504, 0x0000000706050400,
            0x0000070605040100, 0x0000000706050402, 0x0000070605040200, 0x0007060504020100,
            0x0000070605040302, 0x0007060504030200, 0x0706050403020100
        };

        const uint8_t* end = &src[length & ~15];
        do {
            __m128i v = _mm_loadu_si128((__m128i*)src);
            src += 16;

            // detect spaces
            __m128i mask = _mm_cmpeq_epi8(_mm_shuffle_epi8(lut_cntrl, v), v);

            // shift w/blend: each word now only has 3 states instead of 4
            // which reduces the possiblities per qword from 128 to 27
            v = _mm_blendv_epi8(v, _mm_srli_epi16(v, 8), mask);

            // extract bitfields describing each qword: index, popcnt
            __m128i desc = _mm_sad_epu8(_mm_and_si128(mask, sadmask), sadmask);
            size_t lo_desc = (size_t)_mm_cvtsi128_si32(desc);
            size_t hi_desc = (size_t)_mm_extract_epi16(desc, 4);

            // load shuffle control indices from pre-computed table
            __m128i lo_shuf = _mm_loadl_epi64((__m128i*)&table[lo_desc & 0x1F]);
            __m128i hi_shuf = _mm_or_si128(_mm_loadl_epi64((__m128i*)&table[hi_desc & 0x1F]), offset);

            // store an entire qword then advance the pointer by how ever
            // many of those bytes are actually wanted. Any trailing
            // garbage will be overwritten by the next store.
            // note: little endian byte memory order
            _mm_storel_epi64((__m128i*)dst, _mm_shuffle_epi8(v, lo_shuf));
            dst += (lo_desc >> 7);
            _mm_storel_epi64((__m128i*)dst, _mm_shuffle_epi8(v, hi_shuf));
            dst += (hi_desc >> 7);
        } while (src != end);
    }

    // tail loop
    length &= 15;
    if (length != 0) {
        const uint64_t bitmap = 0xFFFFFFFEFFFFC1FF;
        do {
            uint64_t c = *src++;
            *dst = (uint8_t)c;
            dst += ((bitmap >> c) & 1) | ((c + 0xC0) >> 8);
        } while (--length);
    }

    // return pointer to the location after the last element in dst
    return (size_t)(dst - ((uint8_t*)dst_void));
}

Следует ли векторизовать хвостовую петлю или использовать cmov, остается читателю в качестве упражнения. Запись каждого байта безоговорочно/без ветвления выполняется быстро, когда ввод непредсказуем.


Использование AVX2 для создания маски управления тасованием с использованием таблицы в регистре лишь немного медленнее, чем использование больших предварительно вычисленных таблиц.

#include <stdint.h>
#include <immintrin.h>

// probably needs improvment...
size_t despace_avx2_vpermd(const char* src_void, char* dst_void, size_t length)
{
    uint8_t* src = (uint8_t*)src_void;
    uint8_t* dst = (uint8_t*)dst_void;

    const __m256i lut_cntrl2    = _mm256_broadcastsi128_si256(_mm_setr_epi8(' ', 0, 0, 0, 0, 0, 0, 0, 0, '\t', '\n', 0, 0, '\r', 0, 0));
    const __m256i permutation_mask = _mm256_set1_epi64x( 0x0020100884828180 );
    const __m256i invert_mask = _mm256_set1_epi64x( 0x0020100880808080 ); 
    const __m256i zero = _mm256_setzero_si256();
    const __m256i fixup = _mm256_set_epi32(
        0x08080808, 0x0F0F0F0F, 0x00000000, 0x07070707,
        0x08080808, 0x0F0F0F0F, 0x00000000, 0x07070707
    );
    const __m256i lut = _mm256_set_epi32(
        0x04050607, // 0x03020100', 0x000000'07
        0x04050704, // 0x030200'00, 0x0000'0704
        0x04060705, // 0x030100'00, 0x0000'0705
        0x04070504, // 0x0300'0000, 0x00'070504
        0x05060706, // 0x020100'00, 0x0000'0706
        0x05070604, // 0x0200'0000, 0x00'070604
        0x06070605, // 0x0100'0000, 0x00'070605
        0x07060504  // 0x00'000000, 0x'07060504
    );

    // hi bits are ignored by pshufb, used to reject movement of low qword bytes
    const __m256i shuffle_a = _mm256_set_epi8(
        0x7F, 0x7E, 0x7D, 0x7C, 0x7B, 0x7A, 0x79, 0x78, 0x07, 0x16, 0x25, 0x34, 0x43, 0x52, 0x61, 0x70,
        0x7F, 0x7E, 0x7D, 0x7C, 0x7B, 0x7A, 0x79, 0x78, 0x07, 0x16, 0x25, 0x34, 0x43, 0x52, 0x61, 0x70
    );

    // broadcast 0x08 then blendd...
    const __m256i shuffle_b = _mm256_set_epi32(
        0x08080808, 0x08080808, 0x00000000, 0x00000000,
        0x08080808, 0x08080808, 0x00000000, 0x00000000
    );

    for( uint8_t* end = &src[(length & ~31)]; src != end; src += 32){
        __m256i r0,r1,r2,r3,r4;
        unsigned int s0,s1;

        r0 = _mm256_loadu_si256((__m256i *)src); // asrc

        // detect spaces
        r1 = _mm256_cmpeq_epi8(_mm256_shuffle_epi8(lut_cntrl2, r0), r0);

        r2 = _mm256_sad_epu8(zero, r1);
        s0 = (unsigned)_mm256_movemask_epi8(r1);
        r1 = _mm256_andnot_si256(r1, permutation_mask);

        r1 = _mm256_sad_epu8(r1, invert_mask); // index_bitmap[0:5], low32_spaces_count[7:15]

        r2 = _mm256_shuffle_epi8(r2, zero);

        r2 = _mm256_sub_epi8(shuffle_a, r2); // add space cnt of low qword
        s0 = ~s0;

        r3 = _mm256_slli_epi64(r1, 29); // move top part of index_bitmap to high dword
        r4 = _mm256_srli_epi64(r1, 7); // number of spaces in low dword 

        r4 = _mm256_shuffle_epi8(r4, shuffle_b);
        r1 = _mm256_or_si256(r1, r3);

        r1 = _mm256_permutevar8x32_epi32(lut, r1);
        s1 = _mm_popcnt_u32(s0);
        r4 = _mm256_add_epi8(r4, shuffle_a);
        s0 = s0 & 0xFFFF; // isolate low oword

        r2 = _mm256_shuffle_epi8(r4, r2);
        s0 = _mm_popcnt_u32(s0);

        r2 = _mm256_max_epu8(r2, r4); // pin low qword bytes

        r1 = _mm256_xor_si256(r1, fixup);

        r1 = _mm256_shuffle_epi8(r1, r2); // complete shuffle mask

        r0 = _mm256_shuffle_epi8(r0, r1); // despace!

        _mm_storeu_si128((__m128i*)dst, _mm256_castsi256_si128(r0));
        _mm_storeu_si128((__m128i*)&dst[s0], _mm256_extracti128_si256(r0,1));
        dst += s1;
    }
    // tail loop
    length &= 31;
    if (length != 0) {
        const uint64_t bitmap = 0xFFFFFFFEFFFFC1FF;
        do {
            uint64_t c = *src++;
            *dst = (uint8_t)c;
            dst += ((bitmap >> c) & 1) | ((c + 0xC0) >> 8);
        } while (--length);
    }
    return (size_t)(dst - ((uint8_t*)dst_void));
}

Для потомков версия размером 1 КиБ (создание таблицы оставлено читателю в качестве упражнения).

static const uint64_t table[128] __attribute__((aligned(64))) = {
    0x0706050403020100, 0x0007060504030201, ..., 0x0605040302010700, 0x0605040302010007 
};
const __m128i mask_01 = _mm_set1_epi8( 0x01 );

__m128i vector0 = _mm_loadu_si128((__m128i*)src);
__m128i vector1 = _mm_shuffle_epi32( vector0, 0x0E );

__m128i bytemask0 = _mm_cmpeq_epi8( ???, vector0); // detect bytes to omit

uint32_t bitmask0 = _mm_movemask_epi8(bytemask0) & 0x7F7F;
__m128i hsum = _mm_sad_epu8(_mm_add_epi8(bytemask0, mask_01), _mm_setzero_si128());

vector0 = _mm_shuffle_epi8(vector0, _mm_loadl_epi64((__m128i*) &table[(uint8_t)bitmask0]));
_mm_storel_epi64((__m128i*)dst, vector0);
dst += (uint32_t)_mm_cvtsi128_si32(hsum);

vector1 = _mm_shuffle_epi8(vector1, _mm_loadl_epi64((__m128i*) &table[bitmask0 >> 8]));
_mm_storel_epi64((__m128i*)dst, vector1);
dst += (uint32_t)_mm_cvtsi128_si32(_mm_unpackhi_epi64(hsum, hsum));

https://github.com/InstLatx64/AVX512_VPCOMPRESSB_Emu содержит несколько тестов.

person aqrit    schedule 04.08.2017
comment
В табличной версии, вероятно, было бы дешевле сделать dst += _popcnt32(bitmask0 & 0xFF), так как компилятору уже нужны два байта битовой маски в отдельных целочисленных регистрах (для использования в качестве индексов массива). - person Peter Cordes; 08.08.2017
comment
Или вы можете SAD против чего-то другого, кроме setzero, чтобы избежать переключения add с 0/-1 на 1/0. counts = sad(bytemask, set1_epi8(0x80)) добавит 128, если маска равна 0, или 127, если маска равна -1. Таким образом, для каждой 64-битной половины counts это просто popcount = counts - 127*8. Это все еще может скомпилироваться в psadbw xmm0, xmm7 / movq eax, xmm0 / movq xmm1, [table + rax*8 - 127*8*8], поэтому о смещении заботятся бесплатно как часть режима адресации. (Даже без учета размера кода в коде, отличном от PIC, где static const table уже был disp32, а не регистром). - person Peter Cordes; 08.08.2017
comment
Это напоминает мне: _mm_cvtsi128_si64(hsum); может быть _mm_cvtsi128_si32(hsum);, потому что вы знаете, что ваш результат psadbw состоит из нулей за пределами младшего байта. Это позволяет избежать префикса REX (если ваш компилятор сам не определяет оптимизацию) и позволяет тому же коду работать в 32-битном режиме, если это уместно. - person Peter Cordes; 08.08.2017
comment
Вместо vector1 = _mm_unpackhi_epi64(vector0, vector0);, чтобы установить старшую половину вашего вектора для pshufb, вместо этого вы можете сместить индексы перемешивания. _mm_add_epi8( load(table[(bitmask0 >> 8) & 0x7F]), set1_epi8(8)). Это потребует дополнительной константы, но уменьшит нагрузку на порт тасования. (paddb может работать на большем количестве портов, чем punpckhqdq) - person Peter Cordes; 08.08.2017
comment
@PeterCordes - _mm_or_si128(load64(table[..]), set1_epi8(8)) был медленнее на моем Nehalem, не знаю почему, похоже, он должен быть быстрее. movq быстрее для 64-битных сборок, movd для 32-битных сборок popcnt требуется (на практике) sse4.2. подсчет населения в настоящее время не используется для индексации в таблице, будет ли это выгодно? - person aqrit; 09.08.2017
comment
О, хороший момент, я думаю, popcnt поднимет требования с SSSE3 до SSE4.2/popcnt для версии 128b. Я не думаю, что вы можете использовать popcnt как часть индексации таблицы; Я имел в виду, что может быть быстрее popcnt значения, которые вы использовали для индексации таблицы, и удалить _mm_sad / movd материал. - person Peter Cordes; 09.08.2017
comment
медленнее на моем Nehalem: возможно, зависание чтения регистра из-за наличия дополнительного постоянного регистра. (См. руководство Agner Fog по микроархивы). Если movq быстрее в ваших 64-битных сборках, это потому, что ваш компилятор тратит впустую инструкцию, расширяющую результат до нуля? Или, может быть, это проблема декодирования / выравнивания Nehalem. - person Peter Cordes; 09.08.2017
comment
Я поместил ваш код из git на godbolt (godbolt.org/g/33kobf), чтобы увидеть gcc/ лязг выход. Да, _mm_cvtsi128_si32 возвращает значение int, поэтому, если вы не приведете его к (unsigned), компиляторы вставят инструкцию movsx: P. Но с приведением вы получаете меньший размер кода бесплатно. Другие вещи: clang оптимизирует vector1=unpackhi() в pshufd, чтобы сохранить инструкцию movdqa. Компиляторы отдельно маскируют два байта bitmask0. Используйте bitmask0 &= 0x7F7F, чтобы позволить им использовать одну инструкцию AND и две инструкции MOVZX (или AND+MOVZX+SHR, если они не хотят movzx ecx, dh). - person Peter Cordes; 09.08.2017
comment
Также похоже, что цикл clang может сохранить целочисленную инструкцию, используя конечный указатель в качестве границы цикла, поскольку он не использует счетчик цикла (rdx) в качестве индекса. gcc сделает это преобразование за вас. - person Peter Cordes; 09.08.2017

Если кто-то хочет использовать BMI2, доступный на haswell и более поздних версиях, можно использовать pdep, чтобы сначала сжать нежелательные фрагменты из uint64_t, а затем использовать pext, чтобы распределить результат по маске в случайном порядке.

// Step 1 -- replicate mask to nibbles
uint64_t change4 = pdep(change1, 0x1111111111111111ULL) * 0x0F;
// Step 2 -- extract index from array of nibbles
uint64_t indices = pext(0xfedcba09876543210, change4);
// Step 3 -- interleave nibbles to octects
uint64_t high = pdep(indices >> 32ULL,0x0F0F0F0F0F0F0F0F);
uint64_t low = pdep(indices, 0x0F0F0F0F0F0F0F0FULL);
// Step 4 -- use these two masks to compress pSrc
__m128i compressed = _mm_shuffle_epi8(pSrc, _mm_set_epi64(high, low));
// Step 5 -- store 16 bytes unaligned
_mm_storeu_si128(pDst, compressed);
// Step 6 -- increment target pointer
pDst += __mm_popcnt(change1);

Также другие варианты (на основе кумулятивной суммы или сортировки «X» (или нулевых битов) из XX23456789abXXef сначала потребуют некоторой техники для равномерного распределения битов от uint16_t до __m128i (т.

Однако LUT с записью 64k можно разделить на верхнюю и нижнюю части:

int c = change1 & 0xff;
int p = __popcount(c);
uint64_t a = LUT256[c];               // low part of index
uint64_t b = LUT256[change1 >> 8];    // top part of index
b += addlut9[p];                      // 0x0101010101010101 * p
// Then must concatenate b|a at pth position of 'a'
if (p < 8)
{
   a |= b << (8*(8-p));
   b >>= 8*p;
}
__m128i d = _mm_shuffle_epi8(_mm_loadu_si128(pSrc),_mm_set1_epi64(b,a));
// and continue with steps 5 and 6 as before
person Aki Suihkonen    schedule 04.08.2017
comment
На самом деле, решение состоит в том, чтобы разбить бит LUT на 2 маленьких. Большой LUT уничтожает кеш, а 2 маленьких работают нормально. - person Olaf Reusch; 07.08.2017