Де-перемежение канала изображения в 16-битных векторах SSE

байт у меня есть изображение 32 бит на пиксель. Мне нужно перемежать цветовые каналы RGB в разных 16-битных векторах. Для этого я использую следующий код (как деинтерливировать канал изображения в SSE)

  // deinterleave chaneel R, G, B ,A in 16 bits vectors
  {
     __m128i vrgba = _mm_loadu_si128((__m128i *)(pSrc));
     __m128i vr1 = _mm_and_si128(vrgba, _mm_set1_epi32(0xff));
     __m128i vg1 = _mm_and_si128(_mm_srli_epi32(vrgba, 8), _mm_set1_epi32(0xff));
     __m128i vb1 = _mm_and_si128(_mm_srli_epi32(vrgba, 16), _mm_set1_epi32(0xff));
     __m128i va1 = _mm_srli_epi32(vrgba, 24);

     vrgba = _mm_loadu_si128((__m128i *)(pSrc + 4));  // since pSrc is uint32_t type
     __m128i vr2 = _mm_and_si128(vrgba, _mm_set1_epi32(0xff));
     __m128i vg2 = _mm_and_si128(_mm_srli_epi32(vrgba, 8), _mm_set1_epi32(0xff));
     __m128i vb2 = _mm_and_si128(_mm_srli_epi32(vrgba, 16), _mm_set1_epi32(0xff));
     __m128i va2 = _mm_srli_epi32(vrgba, 24);

     vr = _mm_packs_epi32(vr1, vr2);
     vg = _mm_packs_epi32(vg1, vg2);
     vb = _mm_packs_epi32(vb1, vb2);
     va = _mm_packs_epi32(va1, va2);
  }

Можем ли мы сделать это более эффективным? Ниже приведен код для гауссова без де-перемежения каналов. Я нахожу его ужасно неэффективным.

    static inline void ConvertTo16Bits(__m128i& v1, __m128i& v2, const __m128i& v0)
    {
        __m128i const zero = _mm_setzero_si128();
        v1 = _mm_unpacklo_epi8(v0, zero);
        v2 = _mm_unpackhi_epi8(v0, zero);
    }

    static inline void mul32bits(__m128i &vh, __m128i &vl,           // output - 2x4xint32_t
        const __m128i& v0, const __m128i& v1) // input  - 2x8xint16_t
    {
        const __m128i vhi = _mm_mulhi_epu16(v0, v1);
        const __m128i vlo = _mm_mullo_epi16(v0, v1);
        vh = _mm_unpacklo_epi16(vlo, vhi);
        vl = _mm_unpackhi_epi16(vlo, vhi);
    }

    struct Pixel
    {
        unsigned char r;
        unsigned char g;
        unsigned char b;
        unsigned char a;
    };

    void computePixelvalue(unsigned int * pixelArray, int count, unsigned short * gaussArray, Pixel& out)
    {
        __m128i sumRGBA;
        sumRGBA = _mm_set1_epi32(0);
        unsigned int countMod4 = count % 4;
        unsigned int b, g, r, a;
        constexpr int shuffle = _MM_SHUFFLE(3, 1, 0, 0);

        while (count >= 4)
        {
            __m128i vrgba = _mm_loadu_si128((__m128i *)(pixelArray));
            __m128i rgba12, rgba34;

            ConvertTo16Bits(rgba12, rgba34, vrgba);

            unsigned short s1 = *gaussArray++;
            unsigned short s2 = *gaussArray++;

            __m128i shift8 = _mm_set1_epi16(s1);
            __m128i shift16 = _mm_set1_epi16(s2);
            __m128i gaussVector = _mm_shuffle_epi32(_mm_unpacklo_epi32(shift8, shift16), shuffle);

            __m128i multl, multh;
            mul32bits(multl, multh, rgba12, gaussVector);
            sumRGBA = _mm_add_epi32(sumRGBA, multl);
            sumRGBA = _mm_add_epi32(sumRGBA, multh);

            s1 = *gaussArray++;
            s2 = *gaussArray++;
            shift8 = _mm_set1_epi16(s1);
            shift16 = _mm_set1_epi16(s2);
            gaussVector = _mm_shuffle_epi32(_mm_unpacklo_epi32(shift8, shift16), shuffle);

            mul32bits(multl, multh, rgba34, gaussVector);
            sumRGBA = _mm_add_epi32(sumRGBA, multl);
            sumRGBA = _mm_add_epi32(sumRGBA, multh);

            count = count - 4;
            pixelArray = pixelArray + 4;
        }

        r = sumRGBA.m128i_u32[0];
        g = sumRGBA.m128i_u32[1];
        b = sumRGBA.m128i_u32[2];
        a = sumRGBA.m128i_u32[3];

        while (countMod4)
        {
            auto pixelArrayByte = reinterpret_cast<unsigned char*>(pixelArray);

            unsigned short k = static_cast<unsigned short>(*gaussArray++);
            r += *pixelArrayByte++ * k;
            g += *pixelArrayByte++ * k;
            b += *pixelArrayByte++ * k;
            a += *pixelArrayByte++ * k;

            countMod4--;
        }

        out.r = static_cast<unsigned char>(r >> 15);
        out.g = static_cast<unsigned char>(g >> 15);
        out.b = static_cast<unsigned char>(b >> 15);
        out.a = static_cast<unsigned char>(a >> 15);
    }

person Bharat Ahuja    schedule 09.03.2016    source источник
comment
Если вы можете, храните свои данные в плоском формате в первую очередь.   -  person Peter Cordes    schedule 09.03.2016
comment
Из ваших предыдущих вопросов кажется, что вы просто хотите применить фильтр Гаусса, поэтому на самом деле не должно быть необходимости отделять компоненты RGBA таким образом. Вы можете просто применить фильтры к данным в их исходном формате, нет?   -  person Paul R    schedule 09.03.2016
comment
разделение упрощает накопление, не так ли?.. как только я перемежаю, я могу просто использовать что-то вроде этого Rsum = Rsum + Rvector (16 бит * gaussVector 16 бит) и, наконец, я могу поместить его в место назначения как Rsum [0] + Rsum [ 1]+ rСумма[2]. Очевидно, что другим способом было бы сначала сохранить ваши данные в плоском формате, но я думаю, что могу позволить себе копию здесь из-за ограниченной памяти.   -  person Bharat Ahuja    schedule 09.03.2016
comment
Накопление почти одинаково независимо от того, применяете ли вы обратное чередование или нет — единственное небольшое преимущество данных с обращенным чередованием заключается в том, что горизонтальные сдвиги меньше, когда вы применяете фильтр по оси X. С точки зрения производительности, я подозреваю, что вам лучше просто работать с чередующимися данными, и код тоже должен быть намного проще.   -  person Paul R    schedule 10.03.2016
comment
На самом деле я сделал это изначально, но мой код де-перемежения значительно превосходит код чередования. Может быть, вы хотите взглянуть на это?   -  person Bharat Ahuja    schedule 10.03.2016
comment
Попробуйте использовать _mm_set_epi16 вместо ваших ужасных вещей с массивами и неопределенным поведением (*pGauss, *pGauss++, .... Запятые не являются точками последовательности.) Я совсем не удивлен, что второй блок кода, который вы опубликовали, работает медленно. Это не компилируется, поэтому я не могу понять, насколько это плохо. На самом деле, вам может понадобиться загрузить + перетасовать себя для достижения наилучших результатов, если ваш компилятор не видит шаблон в _mm_set_epi16. И почему бы вам не использовать векторное хранилище для результата? Маленькая петля while в конце тоже выглядит неприятно.   -  person Peter Cordes    schedule 10.03.2016
comment
я пытался использовать _mm_set_epi16, но я думаю, что это все еще медленно около 5 мс для изображения 300 * 300 из версии с де-чередованием. Причина, по которой я пытался оптимизировать версию с чередованием, заключается в том, что я написал встроенный ассемблер для mmx, и мой код sse не может обойти этот код для t хотя бы раз, поэтому я думаю, что многое нужно улучшить. в то время как цикл в конце выглядит противно? как ? Я обновил оснастку кода, чтобы она компилировалась, может быть, теперь вы видите, насколько это плохо?   -  person Bharat Ahuja    schedule 10.03.2016
comment
и спасибо за то, что так сильно мне помогли.   -  person Bharat Ahuja    schedule 10.03.2016
comment
Я также использую вектор sumRGBA для хранения суммы, но не использую вектор для окончательного значения пикселя, так как я хочу обрабатывать пиксели отдельно, что помогает только для размытия альфа-канала, где мы можем пропустить пиксели.   -  person Bharat Ahuja    schedule 10.03.2016
comment
последний цикл while неприятный, потому что он не векторизован. Автоматическая векторизация этих умножений и векторное хранилище полностью зависят от компилятора. Получение данных между компонентами в целочисленных элементах regs и vectors происходит очень медленно по сравнению с вертикальными векторными операциями.   -  person Peter Cordes    schedule 11.03.2016
comment
о, я думаю, что последний цикл while — это скалярный цикл очистки. Итак, нвм. Было трудно следить за кодом, так как повсюду были не векторизованные элементы. Теперь он выглядит немного лучше.   -  person Peter Cordes    schedule 11.03.2016
comment
Спасибо Питер. да, последний цикл while - это скалярный цикл очистки. поэтому не могли бы вы предложить, где доступен больший запас оптимизации, поскольку он все еще не может превзойти мой код чередования, который стажер медленнее, чем мой код mmx. может быть, я также могу поделиться всем кодом деинтерливинга, если вы хотите взглянуть?   -  person Bharat Ahuja    schedule 11.03.2016


Ответы (1)


pshufb векторов { a b g r ... } в векторы { a a a a b b b b g g g g r r r r } (по одному pshufb на исходный вектор).

punpckldq между двумя перемешанными исходными векторами, чтобы получить { g2g2g2g2 g1g1g1g1 r2r2r2r2 r1r1r1r1 }. pmovzxbw младшую половину и распаковать старшую половину с нулем, чтобы получить векторы только g и только r.

Точно так же punpckhdq те же два исходных вектора, чтобы получить { a2a2a2a2 a1a1a1a1 b2b2b2b2 b1b1b1b1 }.

Таким образом, на 4 входных вектора (производящих 8 выходных векторов) это:

  • 4x pshufb (все используют одну и ту же маску управления)
  • 2х пунцх/л дк
  • 4x punpckh/l bw (или заменить 2 из них на pmovzxbw)

Всего 10 инструкций ALU, не считая копирования, чтобы избежать уничтожения данных, которые все еще необходимы.

Это довольно хорошо сравнивается с 32 инструкциями, необходимыми для метода маски/сдвига/упаковки. (А без AVX потребуется довольно много копирования, чтобы замаскировать один и тот же вектор четырьмя разными способами.) 8 из этих инструкций являются pack инструкциями по перемешиванию, так что это немного меньше нагрузки на порт для перемешивания в обмен на гораздо более полные инструкции. .

Haswell может выполнять перетасовку только на одном порту выполнения, который не совпадает с портом битового сдвига. (И _mm_and может работать на любом из трех векторных портов выполнения). Я вполне уверен, что способ с 10 перетасовками выиграет с большим отрывом, потому что с ним может перекрываться гораздо больше вычислений.


shufps потенциально полезен как перетасовка из двух исходных векторов, но он имеет 32-битную гранулярность, поэтому я не вижу для него применения. В семействе Intel SnB и семействе AMD Bulldozer нет штрафа за его использование между целочисленными векторными инструкциями.


Другая идея:

__m128i rgba1 = _mm_loadu_si128((__m128i *)(pSrc));   // { a1.4 b1.4 g1.4 r1.4 ... a1.1 b1.1 g1.1 r1.1 }
__m128i rgba2 = _mm_loadu_si128((__m128i *)(pSrc+4)); // { a2.4 b2.4 ... g2.1 r2.1 }

 __m128i rg1 = _mm_and_si128 (rgba1, _mm_set1_epi32(0xffff));
 __m128i rg2 = _mm_slli_epi32(rgba2, 16);

 __m128i rg_interleaved = _mm_or_si128(rg2, rg1);    // { g2.4 r2.4  g1.4 r1.4 ... g2.1 r2.1  g1.1 r1.1 }

Разделите rg_interleaved на расширенные нулями 16-битные векторы r и g с другим _mm_and_si128 и _mm_srli_epi16.

person Peter Cordes    schedule 09.03.2016
comment
Спасибо, Питер, у меня есть идея, но я очень наивен в этом, поэтому будет очень полезно, если вы можете предоставить код для подхода 1. - person Bharat Ahuja; 09.03.2016
comment
@BharatAhuja: Нет, спасибо, определение правильной константы _mm_set_epi8 для _mm_shuffle_epi8 не очень весело. Идите вперед и опубликуйте свой собственный ответ, как только вы его реализуете и протестируете, на благо будущих читателей. Кроме того, если Пол Р. говорит, что вы должны иметь возможность реализовать фильтр Гаусса без распаковки таким образом, то, вероятно, это правда. Вы должны изучить это. Intel опубликовала несколько примеров, таких как software.intel. .com/en-us/search/site/field_tags/ - person Peter Cordes; 09.03.2016