Как преобразовать 24-битный rgb в 32-битный с помощью avx2?

Я сделал это с SSSE3, теперь мне интересно, можно ли это сделать с AVX2 для лучшей производительности?

Я дополняю 24-битный rgb одним нулевым байтом, используя код из Fast 24-битный массив -> преобразование 32-битного массива?.

    static const __m128i mask = _mm_setr_epi8(0, 1, 2, -1, 3, 4, 5, -1, 6, 7, 8, -1, 9, 10, 11, -1);
    for (size_t row = 0; row < height; ++row)
    {
        for (size_t column = 0; column < width; column += 16)
        {
            const __m128i *src = reinterpret_cast<const __m128i *>(in + row * in_pitch + column + (column << 1));
            __m128i *dst = reinterpret_cast<__m128i *>(out + row * out_pitch + (column << 2));
            __m128i v[4];
            v[0] = _mm_load_si128(src);
            v[1] = _mm_load_si128(src + 1);
            v[2] = _mm_load_si128(src + 2);
            v[3] = _mm_shuffle_epi8(v[0], mask);
            _mm_store_si128(dst, v[3]);
            v[3] = _mm_shuffle_epi8(_mm_alignr_epi8(v[1], v[0], 12), mask);
            _mm_store_si128(dst + 1, v[3]);
            v[3] = _mm_shuffle_epi8(_mm_alignr_epi8(v[2], v[1], 8), mask);
            _mm_store_si128(dst + 2, v[3]);
            v[3] = _mm_shuffle_epi8(_mm_alignr_epi8(v[2], v[2], 4), mask);
            _mm_store_si128(dst + 3, v[3]);
        }
    }

Проблема в том, что _mm256_shuffle_epi8 перемешивает старшие 128 бит и младшие 128 бит отдельно, поэтому маску нельзя просто заменить на

    _mm256_setr_epi8(0, 1, 2, -1, 3, 4, 5, -1, 6, 7, 8, -1, 9, 10, 11, -1, 12, 13, 14, -1, 15, 16, 17, -1, 18, 19, 20, -1, 21, 22, 23, -1);

также _mm_alignr_epi8 необходимо заменить на _mm256_permute2x128_si256 и _mm256_alignr_epi8


person Wiki Wang    schedule 11.02.2018    source источник
comment
Вы уже пытались это сделать? Если это так, пожалуйста, опубликуйте свой код до сих пор. Если нет, то, возможно, вы могли бы опубликовать свой существующий код SSE в качестве отправной точки?   -  person Paul R    schedule 11.02.2018
comment
Что именно означает «от 24 бит до 32 бит»? Добавление альфа-компонента? Расширение 8 бит каждого канала до 11,10,11 или 10,12,10?   -  person Jongware    schedule 11.02.2018
comment
Вы имеете в виду 24-битный rgb, дополненный одним нулевым байтом? Для этого вам не нужен ни sse3, ни что-либо еще, потому что это одно и то же.   -  person BalticMusicFan    schedule 12.02.2018
comment
Спасибо за комментарии, ребята, я отредактировал вопрос, чтобы добавить подробности. Пожалуйста, дайте мне знать, если это все еще недостаточно ясно.   -  person Wiki Wang    schedule 12.02.2018
comment
Внутренний характер AVX2 означает, что SSSE3 pshufb все еще может быть лучшим выбором. Но вам следует подумать о невыровненных загрузках вместо использования _mm_alignr_epi8, потому что современный Intel будет узким местом при одном перетасовке за такт, прежде чем он станет узким местом при одном хранилище за такт с вашим кодом, который выполняет несколько перетасовок на хранилище.   -  person Peter Cordes    schedule 12.02.2018
comment
@PeterCordes Одна вещь, о которой я задавался вопросом некоторое время, но никогда не заботилась о том, чтобы проверить, есть ли штраф за пропускную способность при частично перекрывающихся операциях записи. И если да, то можно ли этого избежать с помощью маскировки AVX512. Это то, что я часто делаю, чтобы иметь дело с записями нечетного размера вместо ручного выравнивания. Я предполагаю, что ответ не в снижении пропускной способности, если вы не собираетесь загружать его обратно в ближайшее время. И ни в том, ни в другом случае маскировка не позволит вам избежать зависания форвардинга в том случае, когда вам все-таки нужно загрузить его обратно.   -  person Mysticial    schedule 12.02.2018
comment
@Mysticial - мои тесты не показали штрафа за перекрывающиеся записи. Конечно, могут быть штрафы за невыровненную запись, перекрывающую строку кэша, но нет особого штрафа за запись, которая перекрывает более раннюю запись. Это означает, что вы хотите объединить кучу небольших сегментов байтов, которые имеют нечетные размеры, серия перекрывающихся записей является хорошей стратегией и выполняется по 1 сегменту за цикл (если каждый сегмент помещается в регистр, плюс некоторые штрафы за неизбежные пересечение строки кэша).   -  person BeeOnRope    schedule 13.02.2018
comment
@Mysticial: мое ограниченное тестирование показало то же самое, что и у Би: отсутствие штрафа за пропускную способность за перекрывающиеся записи, кроме границ строки кэша. Я почти уверен, что переадресация в магазин по-прежнему хорошо работает из последнего магазина, независимо от того, перекрываются ли другие более ранние магазины.   -  person Peter Cordes    schedule 13.02.2018


Ответы (2)


Вы можете достаточно эффективно обрабатывать 8 пикселей за раз (24 входных байта и 32 выходных байта) с помощью AVX2.

Вам просто нужно выровнять свои загрузки так, чтобы 24-байтовый блок пикселей, который вы будете обрабатывать, был центрирован в середине 32-байтовой загрузки, а не при обычном подходе выравнивания загрузки по началу. блока пикселей2. Это означает, что граница дорожки будет располагаться между пикселями 4 и 5, и у вас будут байты ровно для 4 пикселов на каждой дорожке. В сочетании с соответствующей маской перемешивания это должно быть в два раза эффективнее, чем SSE.

Например:

Получив указатель ввода uint8_t input[], вы обрабатываете первые четыре пикселя с кодом, отличным от SIMD1, а затем выполняете первую 32-байтную загрузку по адресу input[8], чтобы полоса младшего разряда (байты 0–15) получила 12 байты полезной нагрузки для пикселей 4, 5, 6, 7 в байтах старшего порядка, за которыми сразу следуют следующие 12 байтов полезной нагрузки для следующих 4 пикселей в старшей дорожке. Затем вы используете pshufb, чтобы расширить пиксели до их правильных позиций (вам нужна другая маска для каждой дорожки, так как вы перемещаете пиксели в нижней дорожке в более низкие позиции, а в верхней дорожке в более высокие позиции, но это не не представляет проблемы). Затем следующая загрузка будет в input[26] (через 24 байта) и так далее.

При таком подходе вы должны получить пропускную способность около 8 пикселей за цикл, для идеального кэшированного ввода/вывода - пропускная способность хранилища 1/цикл и пропускная способность перемешивания 1/цикл. К счастью, этот подход совместим с всегда выровненными хранилищами (поскольку приращение хранилища составляет 32 байта). У вас будут некоторые смещенные нагрузки, но они все еще могут возникать при 1/цикле, поэтому не должны быть узким местом.

Стоит отметить, что этот тип подхода «работает только один раз» с точки зрения расширения набора инструкций SIMD: он работает, когда у вас есть 2 полосы, но не более (поэтому та же идея не применима к 512-битному AVX512 с 4 128 битами). -битные дорожки).


1Это позволяет избежать чтения за пределы перед входным массивом: если вы знаете, что это безопасно, вы можете пропустить этот шаг.

2То есть, если вы загружаете из addr, именно addr + 16 должно быть на границе пикселей ((addr + 16) % 12 == 0), а не addr.

person BeeOnRope    schedule 12.02.2018
comment
Спасибо за ответ. Но, хотя полученный результат действительно дает нам правильные результаты, я действительно попробовал этот, и он примерно на 50% медленнее, чем версия SSE, на которую ссылается OP. Я предполагаю, что это связано с комбинацией того факта, что он неизбежно выполняет большинство загрузок без выравнивания и что он по существу отбрасывает 25% всех загруженных данных. Я не рекомендую этот подход. - person Kumputer; 03.02.2019
comment
FWIW, я действительно смог увеличить производительность примерно на 40%, используя тот же код SSE3 и просто включив флаг компилятора AVX, который включает кодирование vex. Похоже, это даст нам наибольшую победу, если только у кого-то еще нет настоящего порта AVX2 версии SSE3. Мы можем обвинить Intel в ограничении границ полосы движения AVX. - person Kumputer; 03.02.2019
comment
@Kumputer - какой тип машины и можете ли вы поделиться своим кодом и тестом? - person BeeOnRope; 03.02.2019
comment
Подробнее в ответе ниже. - person Kumputer; 05.02.2019

Вот исходный код SSSE3, в который добавлены некоторые мои собственные диспетчеризации.

void DspConvertPcm(f32* pOutBuffer, const s24* pInBuffer, size_t totalSampleCount)
{
    constexpr f32 fScale = static_cast<f32>(1.0 / (1<<23));

    size_t i = 0;
    size_t vecSampleCount = 0;

#if defined(SFTL_SSE2)
    if (CpuInfo::GetSupports_SIMD_I32x8())
    {
        vecSampleCount = DspConvertPcm_AVX2(pOutBuffer, pInBuffer, totalSampleCount);
    }
    else
    if (CpuInfo::GetSupports_SSE3())
    {
        const auto vScale = _mm_set1_ps(fScale);
        const auto mask = _mm_setr_epi8(-1, 0, 1, 2, -1, 3, 4, 5, -1, 6, 7, 8, -1, 9, 10, 11);

        constexpr size_t step = 16;
        vecSampleCount = (totalSampleCount / step) * step;

        for (; i < vecSampleCount; i += step)
        {
            const auto* pSrc = reinterpret_cast<const __m128i*>(pInBuffer + i);
            auto* pDst = pOutBuffer + i;

            const auto sa = _mm_loadu_si128(pSrc + 0);
            const auto sb = _mm_loadu_si128(pSrc + 1);
            const auto sc = _mm_loadu_si128(pSrc + 2);

            const auto da = _mm_srai_epi32(_mm_shuffle_epi8(sa, mask), 8);
            const auto db = _mm_srai_epi32(_mm_shuffle_epi8(_mm_alignr_epi8(sb, sa, 12), mask), 8);
            const auto dc = _mm_srai_epi32(_mm_shuffle_epi8(_mm_alignr_epi8(sc, sb,  8), mask), 8);
            const auto dd = _mm_srai_epi32(_mm_shuffle_epi8(_mm_alignr_epi8(sc, sc,  4), mask), 8);

            //  Convert to float and store
            _mm_storeu_ps(pDst + 0,  _mm_mul_ps(_mm_cvtepi32_ps(da), vScale));
            _mm_storeu_ps(pDst + 4,  _mm_mul_ps(_mm_cvtepi32_ps(db), vScale));
            _mm_storeu_ps(pDst + 8,  _mm_mul_ps(_mm_cvtepi32_ps(dc), vScale));
            _mm_storeu_ps(pDst + 12, _mm_mul_ps(_mm_cvtepi32_ps(dd), vScale));
        }
    }
#endif

    for (; i < totalSampleCount; i += 1)
    {
        pOutBuffer[i] = (static_cast<s32>(pInBuffer[i])) * fScale;
    }
}

Если присутствует AVX2, он вызовет DspConvertPcm_AVX2, который выглядит так:

size_t DspConvertPcm_AVX2(f32* pOutBuffer, const s24* pInBuffer, size_t totalSampleCount)
{
    SFTL_ASSERT(CpuInfo::GetSupports_SIMD_I32x8());

    constexpr f32 fScale = static_cast<f32>(1.0 / (1 << 23));
    const auto vScale = _mm256_set1_ps(fScale);

    auto fnDo16Samples = [vScale](f32* pOutBuffer, const s24* pInBuffer)
    {
        const auto vScaleSSE = _mm256_castps256_ps128(vScale);
        const auto mask = _mm_setr_epi8(-1, 0, 1, 2, -1, 3, 4, 5, -1, 6, 7, 8, -1, 9, 10, 11);

        const auto* pSrc = reinterpret_cast<const __m128i*>(pInBuffer);
        auto* pDst = pOutBuffer;

        const auto sa = _mm_loadu_si128(pSrc + 0);
        const auto sb = _mm_loadu_si128(pSrc + 1);
        const auto sc = _mm_loadu_si128(pSrc + 2);

        const auto da = _mm_srai_epi32(_mm_shuffle_epi8(sa, mask), 8);
        const auto db = _mm_srai_epi32(_mm_shuffle_epi8(_mm_alignr_epi8(sb, sa, 12), mask), 8);
        const auto dc = _mm_srai_epi32(_mm_shuffle_epi8(_mm_alignr_epi8(sc, sb, 8), mask), 8);
        const auto dd = _mm_srai_epi32(_mm_shuffle_epi8(_mm_alignr_epi8(sc, sc, 4), mask), 8);

        //  Convert to float and store
        _mm_storeu_ps(pDst +  0, _mm_mul_ps(_mm_cvtepi32_ps(da), vScaleSSE));
        _mm_storeu_ps(pDst +  4, _mm_mul_ps(_mm_cvtepi32_ps(db), vScaleSSE));
        _mm_storeu_ps(pDst +  8, _mm_mul_ps(_mm_cvtepi32_ps(dc), vScaleSSE));
        _mm_storeu_ps(pDst + 12, _mm_mul_ps(_mm_cvtepi32_ps(dd), vScaleSSE));
    };

    //  First 16 samples SSE style
    fnDo16Samples(pOutBuffer, pInBuffer);

    //  Next samples do AVX, where each load will discard 4 bytes at the start and end of each load
    constexpr size_t step = 16;
    const size_t vecSampleCount = ((totalSampleCount / step) * step) - 16;
    {
        const auto mask = _mm256_setr_epi8(-1, 4, 5, 6, -1, 7, 8, 9, -1, 10, 11, 12, -1, 13, 14, 15, -1, 16, 17, 18, -1, 19, 20, 21, -1, 22, 23, 24, -1, 25, 26, 27);
        for (size_t i = 16; i < vecSampleCount; i += step)
        {
            const byte* pByteBuffer = reinterpret_cast<const byte*>(pInBuffer + i);
            auto* pDst = pOutBuffer + i;

            const auto vs24_00_07 = _mm256_loadu_si256(reinterpret_cast<const __m256i*>(pByteBuffer -  4));
            const auto vs24_07_15 = _mm256_loadu_si256(reinterpret_cast<const __m256i*>(pByteBuffer - 24));

            const auto vf32_00_07 = _mm256_srai_epi32(_mm256_shuffle_epi8(vs24_00_07, mask), 8);
            const auto vf32_07_15 = _mm256_srai_epi32(_mm256_shuffle_epi8(vs24_07_15, mask), 8);

            //  Convert to float and store
            _mm256_storeu_ps(pDst + 0, _mm256_mul_ps(_mm256_cvtepi32_ps(vf32_00_07), vScale));
            _mm256_storeu_ps(pDst + 8, _mm256_mul_ps(_mm256_cvtepi32_ps(vf32_00_07), vScale));
        }
    }

    //  Last 16 samples SSE style
    fnDo16Samples(pOutBuffer + vecSampleCount, pInBuffer + vecSampleCount);

    return vecSampleCount;
}

Обратите внимание, что я вручную развернул основной цикл AVX2, чтобы попытаться немного ускорить его, но на самом деле это не имело большого значения.

С таймером, установленным непосредственно перед вызовом DspConvertPcm, который обрабатывает 1024 выборки за раз, среднее время обработки здесь с включенным путем кода AVX2 будет варьироваться от 2,6 до 3,0 микросекунд. С другой стороны, если я отключу путь кода AVX2, среднее время колеблется около 2,0 микросекунд.

С другой стороны, включение кодирования VEX с помощью /arch:AVX2 не дало мне постоянного прироста производительности, о котором я заявлял ранее, так что это, должно быть, было случайностью.

Этот тест был выполнен на процессоре Haswell core i7-6700HQ с частотой 2,6 ГГц с использованием компилятора MSVC по умолчанию в Visual Studio 15.9.5 с включенной оптимизацией для скорости и использованием /fp:fast.

person Kumputer    schedule 04.02.2019
comment
i7-6700HQ — это Skylake, а не Haswell. В любом случае, вы sign-расширяете 24-битное до 32-битного. Это другая проблема от RGB до RGBA или RGB0, где вы хотите заполнить байт A фиксированным значением (в данном случае 0). Этот код выглядит полезным, но опубликован не под тем вопросом. Хотя я подозреваю, что метод @BeeOnRope для выполнения невыровненных загрузок, поэтому вам нужно только одно перемешивание на srai, было бы лучше, особенно на Haswell/Skylake (пропускная способность 1/тактовое перемешивание, по сравнению с 2 на IvyBridge, но все же 2/тактовая пропускная способность). - person Peter Cordes; 04.02.2019
comment
Похоже, вы уже делаете это для версии AVX2, но не для SSSE3, поэтому она будет медленнее на процессорах Haswell/Skylake Pentium/Celeron, которые имеют только SSE4.2. Я не пытался выяснить, что будет быстрее на SnB или Nehalem. Актуальный Core 2 с медленными невыровненными загрузками (даже если он не пересекает границу строки кэша), вероятно, выиграет от palignr, по крайней мере, от Penryn, может быть, и от Conroe, хотя у Conroe медленные тасовки. И кстати, вместо скалярной очистки вы можете использовать перекрывающиеся векторы, если только размер буфера потенциально не меньше, чем один развернутый внутренний цикл. - person Peter Cordes; 04.02.2019