как преобразовать uint32 в uint8, используя simd, но не avx512?

#sse #simd #avx #avx2

#sse #simd #avx #avx2

Вопрос:

Скажем, в выровненной памяти хранится много uint32 uint32 *p , как преобразовать их в uint8 с помощью simd?

Я вижу, что есть _mm256_cvtepi32_epi8/vpmovdb, но он принадлежит avx512, и мой процессор его не поддерживает 😢

Комментарии:

1. Как именно вы хотите их преобразовать? С насыщением или усечением? Каков диапазон 32-разрядных значений?

2. обрезать их до 255

3. Возможно, вам лучше всего начать с vpshufb . Все vpack... инструкции обрабатывают свои входные данные как подписанные, даже если они имеют неназванное насыщение выходных данных (например, vpackusdw ), поэтому 0xFFFFFFFF насыщение со знаком будет 0 (от -1 до 0), а не 0xFFFF (UINT_MAX -> USHORT_MAX)

4. > усечение их до 255 — это ничего не проясняет. Каким должен быть результат преобразования значения 256?

5. я имею в виду просто выбрать младшие 8 бит, 0x87654321 должно быть 0x21

Ответ №1:

Если у вас действительно их много, я бы сделал что-то вроде этого (непроверенный).

Основной цикл считывает 64 байта за итерацию, содержащую 16 значений uint32_t, перетасовывает байты, реализующие усечение, объединяет результат в один регистр и записывает 16 байт с инструкцией векторного хранения.

 void convertToBytes( const uint32_t* source, uint8_t* dest, size_t count )
{
    // 4 bytes of the shuffle mask to fetch bytes 0, 4, 8 and 12 from a 16-bytes source vector
    constexpr int shuffleScalar = 0x0C080400;
    // Mask to shuffle first 8 values of the batch, making first 8 bytes of the result
    const __m256i shuffMaskLow = _mm256_setr_epi32( shuffleScalar, -1, -1, -1, -1, shuffleScalar, -1, -1 );
    // Mask to shuffle last 8 values of the batch, making last 8 bytes of the result
    const __m256i shuffMaskHigh = _mm256_setr_epi32( -1, -1, shuffleScalar, -1, -1, -1, -1, shuffleScalar );
    // Indices for the final _mm256_permutevar8x32_epi32
    const __m256i finalPermute = _mm256_setr_epi32( 0, 5, 2, 7, 0, 5, 2, 7 );

    const uint32_t* const sourceEnd = source   count;
    // Vectorized portion, each iteration handles 16 values.
    // Round down the count making it a multiple of 16.
    const size_t countRounded = count amp; ~( (size_t)15 );
    const uint32_t* const sourceEndAligned = source   countRounded;
    while( source < sourceEndAligned )
    {
        // Load 16 inputs into 2 vector registers
        const __m256i s1 = _mm256_load_si256( ( const __m256i* )source );
        const __m256i s2 = _mm256_load_si256( ( const __m256i* )( source   8 ) );
        source  = 16;
        // Shuffle bytes into correct positions; this zeroes out the rest of the bytes.
        const __m256i low = _mm256_shuffle_epi8( s1, shuffMaskLow );
        const __m256i high = _mm256_shuffle_epi8( s2, shuffMaskHigh );
        // Unused bytes were zeroed out, using bitwise OR to merge, very fast.
        const __m256i res32 = _mm256_or_si256( low, high );
        // Final shuffle of the 32-bit values into correct positions
        const __m256i res16 = _mm256_permutevar8x32_epi32( res32, finalPermute );
        // Store lower 16 bytes of the result
        _mm_storeu_si128( ( __m128i* )dest, _mm256_castsi256_si128( res16 ) );
        dest  = 16;
    }

    // Deal with the remainder
    while( source < sourceEnd )
    {
        *dest = (uint8_t)( *source );
        source  ;
        dest  ;
    }
}
  

Комментарии:

1. Если вы правильно упорядочите свои перетасовки epi8, вы должны быть в состоянии выполнить окончательное перетасование на res16 32> 16 байт с помощью единицы vpermd (или, может быть, даже vpermq ), а не vextracti128 vpor . Если вы не настраиваетесь на Zen1 (где извлечение по дорожке очень дешево), просто 1 shuffle лучше, чем shuffle или.

2. Хм, другой альтернативой были бы нагрузки с другим выравниванием для подачи смеси байтов vpshufb vpermd . IDK, если это лучше, хотя Skylake работает vpblendvb как 2 uops для любого порта ALU. С 64-байтовым выровненным источником вы можете упорядочить его так, чтобы ни одна из загрузок не разбивалась на строки кэша.

3. @PeterCordes Я бы не стал связываться с нагрузками. Единственная причина, по которой последовательная загрузка ОЗУ происходит быстро, — это предварительная выборка в процессорах, последовательный доступ с плотным выравниванием — лучший вариант для этого аппаратного обеспечения. Как только вы начнете вводить смещения, вы окажетесь во власти реализации, которая может выполнять или не выполнять хорошую работу с точки зрения производительности.

4. Интересный момент, который, возможно, может отключить предварительную выборку L1d. Но основные средства предварительной выборки находятся в L2, и они видят только поток запросов из L1 для полных строк кэша. Но я бы предположил, что даже предварительная выборка L1d, вероятно, все равно будет в порядке; у вас есть развернутый цикл, в котором каждая загрузка видит смещение в 64 байта с момента последней итерации; тот факт, что нагрузки смещены друг от друга на 31 байт, не имеет большого значения. Я думаю, что был еще один вопрос и ответ, где кто-то реализовал аналогичную чередующуюся пару слегка перекрывающихся нагрузок смеси для решения аналогичной проблемы с хорошими результатами.