Как ускорить предварительное умножение альфа-функции с помощью SIMD-инструкций? - PullRequest
7 голосов
/ 03 июня 2019

Мне нужен совет SSE / AVX по оптимизации подпрограммы, которая предварительно умножает канал RGB с его альфа-каналом: RGB * alpha / 255 (+ мы сохраняем оригинальный альфа-канал).

    for (int i = 0, max = width * height * 4; i < max; i+=4) {
        data[i] = static_cast<uint16_t>(data[i] * data[i+3]) / 255;
        data[i+1] = static_cast<uint16_t>(data[i+1] * data[i+3]) / 255;
        data[i+2] = static_cast<uint16_t>(data[i+2] * data[i+3]) / 255;
    }

Ниже вы найдете мою текущую реализацию, но я думаю, что это может быть намного быстрее, и я трачу драгоценные циклы процессора.Я проверил его на quick-bench.com, и он показывает обнадеживающие результаты, но что я должен изменить, чтобы сделать его быстрым?

Спасибо

-------- ОБНОВЛЕНО09/06/2019 --------

На основе комментариев @chtz и @Peter Cordes я собрал репозиторий , чтобы оценить различные решения.Результаты.Как вы думаете, может быть лучше?

Run on (8 X 3100 MHz CPU s)
CPU Caches:
  L1 Data 32K (x4)
  L1 Instruction 32K (x4)
  L2 Unified 262K (x4)
  L3 Unified 8388K (x1)
Load Average: 1.24, 1.60, 1.68
-----------------------------------------------------------------------------
Benchmark                   Time             CPU   Iterations UserCounters...
-----------------------------------------------------------------------------
v1_plain_mean         1189884 ns      1189573 ns         1000 itr=840.865/s
v1_plain_median       1184059 ns      1183786 ns         1000 itr=844.747/s
v1_plain_stddev         20575 ns        20166 ns         1000 itr=13.4227/s

v1_simd_x86_mean       297866 ns       297784 ns         1000 itr=3.3616k/s
v1_simd_x86_median     294995 ns       294927 ns         1000 itr=3.39067k/s
v1_simd_x86_stddev       9863 ns         9794 ns         1000 itr=105.51/s

Thanks Dot and Beached (discord #include)
v2_plain_mean          323541 ns       323451 ns         1000 itr=3.09678k/s
v2_plain_median        318932 ns       318855 ns         1000 itr=3.13623k/s
v2_plain_stddev         13598 ns        13542 ns         1000 itr=122.588/s

Thanks Peter Cordes (stackoverflow)
v3_simd_x86_mean       264323 ns       264247 ns         1000 itr=3.79233k/s
v3_simd_x86_median     260641 ns       260560 ns         1000 itr=3.83788k/s
v3_simd_x86_stddev      12466 ns        12422 ns         1000 itr=170.36/s

Thanks chtz (stackoverflow)
v4_simd_x86_mean       266174 ns       266109 ns         1000 itr=3.76502k/s
v4_simd_x86_median     262940 ns       262916 ns         1000 itr=3.8035k/s
v4_simd_x86_stddev      11993 ns        11962 ns         1000 itr=159.906/s

-------- ОБНОВЛЕНО 10/06/2019 --------

Я добавил версию AVX2 и использовал подсказку chtz.Используя 255 для альфа-значения в color_odd, я смог удалить _mm_blendv_epi8 и улучшить тест.

Спасибо Питеру и chtz

v3_simd_x86_mean       246171 ns       246107 ns          100 itr=4.06517k/s
v3_simd_x86_median     245191 ns       245167 ns          100 itr=4.07885k/s
v3_simd_x86_stddev       5423 ns         5406 ns          100 itr=87.13/s

// AVX2
v5_simd_x86_mean       158456 ns       158409 ns          100 itr=6.31411k/s
v5_simd_x86_median     158248 ns       158165 ns          100 itr=6.3225k/s
v5_simd_x86_stddev       2340 ns         2329 ns          100 itr=92.1406/s

Ответы [ 2 ]

3 голосов
/ 06 июня 2019

Я немного поиграл с этим.Я думаю, что лучшее решение состоит в том, чтобы разделить входные данные из двух регистров на каналы из 16-битных целых чисел (то есть, 8-битное целое число с чередованием 0x00).Затем выполните фактическое масштабирование (используя только 6 умножений + 3 сдвига для 8 пикселей вместо 8 + 4 в исходном подходе), а затем снова объедините каналы в пиксели.

Подтверждение концепции (не проверено)при условии, что ввод выровнен и число пикселей кратно 8, версия 2.0 (см. history для предыдущей версии):

void alpha_premultiply(__m128i *input, int length)
{
    for(__m128i* last = input + (length & ~1); input!=last; input+=2)
    {
        // load data and split channels:
        __m128i abgr = _mm_load_si128(input);
        __m128i ABGR = _mm_load_si128(input+1);
        __m128i __ab = _mm_srli_epi32(abgr,16);
        __m128i GR__ = _mm_slli_epi32(ABGR,16);
        __m128i ABab = _mm_blend_epi16(ABGR, __ab, 0x55);
        __m128i GRgr = _mm_blend_epi16(GR__, abgr, 0x55);
        __m128i A_a_ = _mm_and_si128(ABab, _mm_set1_epi16(0xFF00));
        __m128i G_g_ = _mm_and_si128(GRgr, _mm_set1_epi16(0xFF00));
        __m128i R_r_ = _mm_slli_epi16(GRgr, 8);
        __m128i B_b_ = _mm_slli_epi16(ABab, 8);

        // actual alpha-scaling:
        __m128i inv = _mm_set1_epi16(0x8081); // = ceil((1<<(16+7))/255.0)
        G_g_ = _mm_mulhi_epu16(_mm_mulhi_epu16(G_g_, A_a_), inv);
        // shift 7 to the right and 8 to the left, or shift 1 to the left and mask:
        G_g_ = _mm_and_si128(_mm_add_epi16(G_g_, G_g_), _mm_set1_epi16(0xFF00));
        __m128i _R_r = _mm_mulhi_epu16(_mm_mulhi_epu16(R_r_, A_a_), inv);
        _R_r = _mm_srli_epi16(_R_r,7);
        __m128i _B_b = _mm_mulhi_epu16(_mm_mulhi_epu16(B_b_, A_a_), inv);
        _B_b = _mm_srli_epi16(_B_b,7);

        // re-assemble channels:
        GRgr = _mm_or_si128(_R_r, G_g_);
        ABab = _mm_or_si128(A_a_, _B_b);

        __m128i __GR = _mm_srli_epi32(GRgr, 16);
        __m128i ab__ = _mm_slli_epi32(ABab, 16);

        ABGR = _mm_blend_epi16(ABab, __GR, 0x55);
        abgr = _mm_blend_epi16(ab__, GRgr, 0x55);

        // store result
        _mm_store_si128(input, abgr);
        _mm_store_si128(input+1, ABGR);
    }
}

Имена переменных используют _ для обозначения 0,и младший байт адреса находится справа (чтобы быть менее запутанным со сдвигом и битовыми операциями).Каждый регистр будет содержать 4 последовательных пикселя или 4 + 4 чередующихся канала.Строчные и прописные буквы находятся в разных местах ввода.(Godbolt: https://godbolt.org/z/OcxAfJ)

В Haswell (или ранее) это было бы узким местом на порту 0 (сдвиг и умножение), но с SSSE3 вы могли бы заменить все 8- и 16-смены на _mm_alignr_epi8. Ибыло бы лучше оставить _R_r и _B_b на младших байтах (использует pand вместо psllw, но требует смещения A_a_ на _A_a). Возможная ловушка: clang заменяет _mm_alignr_epi8с помощью соответствующих инструкций смены: https://godbolt.org/z/BhEZoV (возможно, существуют флаги, запрещающие лязгу заменять их. GCC использует palignr: https://godbolt.org/z/lu-jNQ)

На Skylake это может быть оптимальным, как есть (за исключениемконечно, портирование на AVX2. Существует 8 смен, 6 умножений и 1 сложение, т. е. 15 операций на портах 0 и 1. Кроме того, 4 операции на порте 5 и 5 и / или операции (4 на p5 и другая на любомp0 или p1), т. е. 8 моп на порт для 8 пикселей (или 16 пикселей для AVX2).

Код должен быть очень простым для переноса на AVX2 (и использование только AVX1 позволит сохранить некоторые копии регистров)., чтобы сделать код SSE2 совместимым, толькоИнструкции смешивания должны быть заменены соответствующими и + или операциями.

3 голосов
/ 04 июня 2019

Если вы можете использовать SSSE3, _mm_shuffle_epi8 позволяет вам создать вектор __m128i alpha вместо AND / shift / OR.

pshufb обнулит байты, в которых установлен старший бит элемента вектора управления случайным порядком. (Пропускная способность в случайном порядке легко является узким местом в Intel Haswell и более поздних версиях, поэтому использование немедленных сдвигов или AND по-прежнему хорошо для других операций, где вы можете выполнить это одной инструкцией.)

В Skylake и более поздних версиях, вероятно, выгодно использовать SSE4.1 pblendvb для объединения альфы вместо AND / ANDN / OR. (В Haswell 2 мопа pblendvb могут работать только на 5-м порту. На самом деле это может быть нормально, потому что других мопов достаточно, чтобы это не создало случайное узкое место.)

На Skylake не-VEX pblendvb - это команда с одним битом, которая выполняется на любом порту. (Версия VEX составляет 2 мопа для любого порта, поэтому она все еще строго лучше, чем AND / ANDN / OR, но не так хороша, как версия SSE. Хотя версия SSE использует неявный ввод XMM0, поэтому она стоит дополнительно movdqa инструкции, если только ваш цикл не использует pblendvb с той же маской наложения. Или, если вы развернете, то, возможно, можно * амортизировать movdqa для установки XMM0.)


Кроме того, _mm_srli_epi16 на 7 и _mm_slli_epi16(color_odd, 8) может быть только одной сменой, возможно с AND. Или pblendvb избавляет от необходимости убирать мусор, как вы делаете это перед операцией OR.

Я не уверен, что вы могли бы использовать _mm_mulhrs_epi16 для многократной смены, но, вероятно, нет. Это не правильный сдвиг, и +1 для «округления» - это не то, что вам нужно.


Очевидно, что эта версия AVX2 может выполнять вдвое больше работы на инструкцию, обеспечивая ускорение в 2 раза на Haswell / Skylake для основного цикла. Вероятно, в некоторой степени нейтрален Райзен, где инструкции 256b декодируются в 2 мопа. (Или больше для перестановок, пересекающих полосу, но у вас их нет.)

Очистка в худшем случае должна выполняться больше раз, но она все равно должна быть незначительной.

...