Самое быстрое масштабирование на 50% (A) изображений RGB32 с использованием встроенных функций sse

#c #sse

#c #sse

Вопрос:

Я хочу уменьшить масштаб изображений так быстро, как только смогу на c . В этой статье описывается, как эффективно уменьшить среднее значение 32-битных изображений rgb на 50%. Это быстро и выглядит хорошо.

Я попытался изменить этот подход, используя встроенные функции sse. Приведенный ниже код работает независимо от того, включен SSE или нет. Удивительно, однако, что ускорение незначительно.

Кто-нибудь может увидеть способ улучшить код SSE. Две строки, создающие переменные shuffle1 и shuffle2, кажутся двумя кандидатами (с использованием некоторого умного сдвига или подобного).

 /*
 * Calculates the average of two rgb32 pixels.
 */
inline static uint32_t avg(uint32_t a, uint32_t b)
{
    return (((a^b) amp; 0xfefefefeUL) >> 1)   (aamp;b);
}

/*
 * Calculates the average of four rgb32 pixels.
 */
inline static uint32_t avg(const uint32_t a[2], const uint32_t b[2])
{
    return avg(avg(a[0], a[1]), avg(b[0], b[1]));
}

/*
 * Calculates the average of two rows of rgb32 pixels.
 */
void average2Rows(const uint32_t* src_row1, const uint32_t* src_row2, uint32_t* dst_row, int w)
{
#if !defined(__SSE)
        for (int x = w; x; --x, dst_row  , src_row1  = 2, src_row2  = 2)
            * dst_row = avg(src_row1, src_row2);
#else
        for (int x = w; x; x-=4, dst_row =4, src_row1  = 8, src_row2  = 8)
        {
            __m128i left  = _mm_avg_epu8(_mm_load_si128((__m128i const*)src_row1), _mm_load_si128((__m128i const*)src_row2));
            __m128i right = _mm_avg_epu8(_mm_load_si128((__m128i const*)(src_row1 4)), _mm_load_si128((__m128i const*)(src_row2 4)));

            __m128i shuffle1 = _mm_set_epi32( right.m128i_u32[2], right.m128i_u32[0], left.m128i_u32[2], left.m128i_u32[0]);
            __m128i shuffle2 = _mm_set_epi32( right.m128i_u32[3], right.m128i_u32[1], left.m128i_u32[3], left.m128i_u32[1]);

            _mm_store_si128((__m128i *)dst_row, _mm_avg_epu8(shuffle1, shuffle2));
        }
#endif
}
  

Ответ №1:

Передача данных между регистрами общего назначения и регистрами SSE происходит очень медленно, поэтому вам следует воздержаться от таких действий, как :

 __m128i shuffle1 = _mm_set_epi32( right.m128i_u32[2], right.m128i_u32[0], left.m128i_u32[2], left.m128i_u32[0]);
__m128i shuffle2 = _mm_set_epi32( right.m128i_u32[3], right.m128i_u32[1], left.m128i_u32[3], left.m128i_u32[1]);
  

Перетасуйте значения в регистрах SSE с помощью соответствующих операций перетасовки.

Это должно быть то, что вы ищете :

 __m128i t0 = _mm_unpacklo_epi32( left, right ); // right.m128i_u32[1] left.m128i_u32[1] right.m128i_u32[0] left.m128i_u32[0]
__m128i t1 = _mm_unpackhi_epi32( left, right ); // right.m128i_u32[3] left.m128i_u32[3] right.m128i_u32[2] left.m128i_u32[2]
__m128i shuffle1 = _mm_unpacklo_epi32( t0, t1 );    // right.m128i_u32[2] right.m128i_u32[0] left.m128i_u32[2] left.m128i_u32[0]
__m128i shuffle2 = _mm_unpackhi_epi32( t0, t1 );    // right.m128i_u32[3] right.m128i_u32[1] left.m128i_u32[3] left.m128i_u32[1]
  

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

1. Прекрасно, вы решили эту проблему. Эти две строки увеличили скорость на 125%. Теперь алгоритм ограничен только пропускной способностью.

Ответ №2:

Основная проблема заключается в использовании _mm_set_epi32 для выполнения перетасовки — в отличие от большинства встроенных функций SSE, это не сопоставляется напрямую с одной инструкцией SSE — в таких случаях, как этот, генерируется много скалярного кода под капотом и заставляет данные перемещаться между памятью, регистрами общего назначения и регистрами SSE. Вместо этого посмотрите на использование соответствующих встроенных функций SSE shuffle.

Вторичная проблема заключается в том, что вы выполняете очень мало вычислений относительно количества загрузок и хранилищ. Это, как правило, приводит к коду, ограниченному пропускной способностью, а не вычислениями, и вы можете не увидеть значительного улучшения производительности даже при идеальном коде SSE. Посмотрите на объединение большего количества операций в вашем цикле, чтобы вы могли больше работать со своими данными, пока они находятся в кэше.

Ответ №3:

Если встроенные функции SSE практически ничего не меняют, то код, вероятно, ограничен пропускной способностью памяти.

В вашем коде есть много загрузок и хранилищ ( _mm_set_epi32 это нагрузка, а также очевидные) для небольшой фактической работы.

Если загрузка / сохранение занимает все время выполнения, то никакое количество сложных инструкций вас не спасет. На современных процессорах с высокой степенью конвейеризации и переупорядочения инструкций, вероятно, выполняется довольно хорошая работа по обеспечению занятости всего процессора в версии вашего кода, отличной от SSE.

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