AVX2 SIMD Instrinsics 16-битный или 8-битный наоборот - PullRequest
4 голосов
/ 19 октября 2019

У меня есть функция c ++ (или c-like), ниже которой я пытаюсь векторизовать. Эта функция является одним из многих вариантов компоновки изображений, когда она берет плоскость изображения Y, U или V с подвыборкой цветности 444 и комбинирует / накладывает изображение src на изображение dst (где изображение src также содержит альфа-прозрачность).

#include <cstdint>


void composite(uint8_t *__restrict__ pSrc,  // Source plane
               uint8_t *__restrict__ pSrcA, // Source alpha plane 
               uint8_t *__restrict__ pDst,  // Destination plane
               const std::size_t nCount)    // Number of component pixels to process.
{
    for (std::size_t k = 0; k < nCount; ++k)
    {
        uint16_t w = (pSrc[k] * pSrcA[k]);
        uint16_t x = (255 - pSrcA[k]) * pDst[k];
        uint16_t y = w+x;
        uint16_t z = y / uint16_t{255};
        pDst[k] = static_cast<uint8_t>(z);
    }
}

В векторизованном эквиваленте AVX2 я изо всех сил пытаюсь понять, как эффективно считывать 8-битные преобразования в 16-битные и (после обработки / компоновки), чтобы наконец преобразовать 16-битные выборки обратно в 8-битные. немного, чтобы сохранить обратно в память. Что касается чтения, я использую промежуточный регистр xmm - это не самый лучший подход;Я предполагаю, что при смешивании семейств регистров будет снижение производительности.

Я пришел с (неполным):

#include <cstdint>

#include <immintrin.h>
#include <emmintrin.h>


///////////////////////////////////////////////////////////////////////////
// Credit: https://stackoverflow.com/questions/35285324/how-to-divide-16-bit-integer-by-255-with-using-sse
#define AVX2_DIV255_U16(x) _mm256_srli_epi16(_mm256_mulhi_epu16(x, _mm256_set1_epi16((short)0x8081)), 7)

///////////////////////////////////////////////////////////////////////////
/// Blends/composites/overlays two planes of Y, U, or V plane with 4:4:4 chroma subsampling over the other.
/// \param d The destination Y, U , or V component
/// \param s The source Y, U, or V component
/// \param sa The source alpha component
/// \param pixels The number of pixels that require processing.
/// \return The number of pixels processed.
int blend_plane_pixels_444_vectorized(uint8_t *__restrict__ d,
                                      uint8_t *__restrict__ s,
                                      uint8_t *__restrict__ sa,
                                      const int pixels)
{
    int n = 0; // Return number of component pixels processed.
    for (int k = 0; k + 32 <= pixels; k += 32)
    {
        // Load first 16 (unaligned) of d, s, sa
        // TODO: This efficient mixing xmm registers with ymm??
        auto vecD0 = _mm256_cvtepu8_epi16(_mm_loadu_si128((__m128i_u *)d));
        auto vecS0 = _mm256_cvtepu8_epi16(_mm_loadu_si128((__m128i_u *)s));
        auto vecSa0 = _mm256_cvtepu8_epi16(_mm_loadu_si128((__m128i_u *)sa));

        // Load second 16 (unaligned) of d, s, sa
        auto vd1 = _mm256_cvtepu8_epi16(_mm_loadu_si128((__m128i_u *)d+16));
        auto vs1 = _mm256_cvtepu8_epi16(_mm_loadu_si128((__m128i_u *)s+16));
        auto vsa1 = _mm256_cvtepu8_epi16(_mm_loadu_si128((__m128i_u *)sa+16));

        // Load 255 into register
        auto vec255 = _mm256_set1_epi16(255);

        // uint16_t w = (pSrc[k] * pSrcA[k]);
        auto vecW0 = _mm256_mullo_epi16(vecS0, vecSa0);
        auto vecW1 = _mm256_mullo_epi16(vs1, vsa1);

        // uint16_t x = (255 - pSrcA[k]) * pDst[k];
        auto vecX0 = _mm256_mullo_epi16(_mm256_subs_epu16(vec255, vecSa0), vecD0);
        auto vecX1 = _mm256_mullo_epi16(_mm256_subs_epu16(vec255, vsa1), vd1);

        // Load 127 into register
        auto vec127 = _mm256_set1_epi16(127);

        // uint16_t y = w+x;
        auto vecY0 = _mm256_adds_epu16(_mm256_adds_epu16(vecW0, vecX0), vec127);
        auto vecY1 = _mm256_adds_epu16(_mm256_adds_epu16(vecW1, vecX1), vec127);

        // uint16_t z = y / uint16_t{255};
        auto vecZ0 = AVX2_DIV255_U16(vecY0);
        auto vecZ1 = AVX2_DIV255_U16(vecY1);

        // TODO: How to get this back into 8-bit samples so that it can be stored
        //       back into array.
        auto vecResult = _mm256_blendv_epi8(vecZ0, vecZ1, _mm256_set1_epi16(127));

        // Write data back to memory (unaligned)
        _mm256_storeu_si256((__m256i*)d, vecResult);

        d += 32;
        s += 32;
        sa += 32;
        n += 32;
    }

    return n;
}

SIMD не моя сильная сторона, и это что-тоМне нужно поправиться - пожалуйста, будьте нежны. Я предполагаю, что, вероятно, есть много настроек, которые я мог бы применить к текущему векторизованному коду (предложения приветствуются!)

Среда разработки:

  • Linux Ubuntu 18.04
  • G ++ v8. 3,0
  • с ++ 14

1 Ответ

5 голосов
/ 19 октября 2019

Как правило, если вам нужно повторно упаковать результат в 8-битные целые числа, вам лучше либо распаковать с нулем, используя punpcklbw / punpckhbw, либо переупаковать результат, используя packuswb. Или иногда вы можете замаскировать нечетные и четные байты в отдельные регистры, выполнить вычисления и битовые или результаты вместе.

«Проблема» с _mm256_cvtepu8_epi16 / vpmovzxbw заключается в том, чтопересечение (т. е. он принимает входные данные только из нижней 128-битной половины (или памяти), но результат находится в верхней и нижней половине), и не существует (простого) решения для объединения 16-битных значений из разных дорожек обратно водин (до тех пор, пока AVX512 не пересекает полосу с одним регистром инструкции с насыщением или усечением).

В вашем случае вы можете фактически собрать вместе значения d и s в один регистр и a и255-a значения в другом и использовать vpmaddubsw для умножения и сложения. Вам необходимо вычесть 128 из значений d и s перед упаковкой их вместе, поскольку один аргумент должен быть со знаком int8. Результат будет отключен на 128*255, но это может быть компенсировано, особенно если вы все равно добавите 127 для округления. (Если вы этого не сделаете, вы можете добавить 128 к каждому байту после деления (деление со знаком с округлением вниз) и переупаковки.

Непроверенный код, используя ту же подпись, что и ваша попытка:

// https://stackoverflow.com/questions/35285324/how-to-divide-16-bit-integer-by-255-with-using-sse
inline __m256i div255_epu16(__m256i x) {
    __m256i mulhi = _mm256_mulhi_epu16(x, _mm256_set1_epi16(0x8081));
    return _mm256_srli_epi16(mulhi, 7);
}

int blend_plane_pixels_444_vectorized(uint8_t *__restrict__ d,
                                      uint8_t *__restrict__ s,
                                      uint8_t *__restrict__ sa,
                                      const int pixels)
{
    int n = 0; // Return number of component pixels processed.
    for (int k = 0; k + 32 <= pixels; k += 32)
    {
        // Load 32 (unaligned) of d, s, sa
        __m256i vecD = _mm256_loadu_si256((__m256i_u *)d);
        __m256i vecS = _mm256_loadu_si256((__m256i_u *)s );
        __m256i vecA = _mm256_loadu_si256((__m256i_u *)sa);

        // subtract 128 from D and S to have them in the signed domain
        // subtracting 128 is equivalent ot xor with 128
        vecD = _mm256_xor_si256(vecD, _mm256_set1_epi8(0x80));
        vecS = _mm256_xor_si256(vecS, _mm256_set1_epi8(0x80));

        // calculate 255-a (equivalent to 255 ^ a):
        __m256i vecA_ = _mm256_xor_si256(vecA, _mm256_set1_epi8(0xFF));

        __m256i vecAA_lo = _mm256_unpacklo_epi8(vecA, vecA_);
        __m256i vecSD_lo = _mm256_unpacklo_epi8(vecS, vecD);
        __m256i vecAA_hi = _mm256_unpackhi_epi8(vecA, vecA_);
        __m256i vecSD_hi = _mm256_unpackhi_epi8(vecS, vecD);

        // R = a * (s-128) + (255-a)*(d-128) = a*s + (255-a)*d - 128*255
        __m256i vecR_lo = _mm256_maddubs_epi16(vecAA_lo,vecSD_lo);
        __m256i vecR_hi = _mm256_maddubs_epi16(vecAA_hi,vecSD_hi);

        // shift back to unsigned domain and add 127 for rounding
        vecR_lo = _mm256_add_epi16(vecR_lo, _mm256_set1_epi16(127+128*255));
        vecR_hi = _mm256_add_epi16(vecR_hi, _mm256_set1_epi16(127+128*255));

        // divide (rounding down)
        vecR_lo = div255_epu16(vecR_lo);
        vecR_hi = div255_epu16(vecR_hi);

        // re-join lower and upper half:
        __m256i vecResult = _mm256_packus_epi16(vecR_lo, vecR_hi);
        // Write data back to memory (unaligned)
        _mm256_storeu_si256((__m256i*)d, vecResult);

        d += 32;
        s += 32;
        sa += 32;
        n += 32;
    }

    return n;
}

Godbolt-Link: https://godbolt.org/z/EYzLw2 Обратите внимание, что -march=haswell или любая архитектура, которую вы хотите поддерживать, имеет решающее значение, потому что в противном случае gcc не будет использовать невыровненные данные в качестве операнда источника памяти. Конечно, применяются общие правила векторизации, т.е. , если у вас есть контроль над выравниванием, предпочтите выделение выровненных данных. А если нет, вы можете очистить первые невыровненные байты (например, от d), чтобы иметь хотя бы одну загрузку и выровнять хранилище.

Clang развернет цикл (до двух внутренних итераций), что немного улучшит производительность при достаточно большом вводе.

...