Преобразование вектора float в 16-битный int без насыщения

Я хочу преобразовать значение с плавающей запятой в 16-разрядное целое без знака без насыщения (вместо wrapparound/overflow).

#include <iostream>
#include <xmmintrin.h>

void satur_wrap()
{
    const float bigVal = 99000.f;
    const __m128 bigValVec = _mm_set1_ps(bigVal);

    const __m64 outVec64 =_mm_cvtps_pi16(bigValVec);

#if 0
    const __m128i outVec = _mm_movpi64_epi64(outVec64);
#else

    #if 1
        const __m128i outVec  = _mm_packs_epi32(_mm_cvttps_epi32(bigValVec), _mm_cvttps_epi32(bigValVec));
    #else
        const __m128i outVec  = _mm_cvttps_epi32(bigValVec);
    #endif

#endif

    uint16_t *outVals = NULL;
    posix_memalign((void **) &outVals, sizeof(__m128i), sizeof(__m128i));

    _mm_store_si128(reinterpret_cast<__m128i *>(outVals), outVec);

    for (int i = 0; i < sizeof(outVec) / sizeof(*outVals); i++)
    {
        std::cout << "outVals[" << i << "]: " << outVals[i] << std::endl;
    }

    std::cout << std::endl
        << "\tbigVal: " << bigVal << std::endl
        << "\t(unsigned short) bigVal: " << ((unsigned short) bigVal)  << std::endl
        << "\t((unsigned short)((int) bigVal)): " << ((unsigned short)((int) bigVal)) << std::endl
        << std::endl;
}

Пример выполнения:

$ ./row
outVals[0]: 32767
outVals[1]: 32767
outVals[2]: 32767
outVals[3]: 32767
outVals[4]: 32767
outVals[5]: 32767
outVals[6]: 32767
outVals[7]: 32767

        bigVal: 99000
        (unsigned short) bigVal: 65535
        ((unsigned short)((int) bigVal)): 33464

Оператор ((unsigned short)((int) bigVal)) работает по желанию (но, вероятно, UB, правильно?). Но я не могу найти нечто подобное с SSE. Я должен что-то пропустить, но я не смог найти примитив для преобразования четырех 32-разрядных float в четыре 32-битных int s.


EDIT: Ой, я полагал, что это будет "нормальным" для 32-разрядного целочисленного → 16-разрядного беззнакового целочисленного преобразования для использования wraparound. Но с тех пор я узнал, что _mm_packs_epi32 использует unsigned-saturate (и не существует _mm_packus_epi32). Есть ли способ установить режим или другой примитив, кроме _mm_packus_epi32?

Ответы

Ответ 1

Я отвечаю только на вопрос о 32-битном целочисленном → 16-разрядном преобразовании без знака без знака.

Так как вам нужен обход, просто возьмите младшее слово каждого двойного слова, содержащего 32-битное целое число. Эти 16-разрядные целые числа чередуются с 16-разрядными фрагментами неиспользуемых данных, поэтому удобно их упаковывать в смежный массив. Самый простой способ сделать это - использовать _mm_shuffle_epi8 intrinsic (SSSE3).

Если вы хотите, чтобы ваша программа была более переносимой и требовала только набора инструкций SSE2, вы можете упаковать значения с помощью _mm_packs_epi32, но отключите ее насыщающее поведение следующим трюком:

x = _mm_slli_epi32(x, 16);
y = _mm_slli_epi32(y, 16);

x = _mm_srai_epi32(x, 16);
y = _mm_srai_epi32(y, 16);

x = _mm_packs_epi32(x, y);

Этот трюк работает, потому что он выполняет расширение знака 16-битных значений, что делает подписанную насыщенность no-op.

Тот же трюк работает с _mm_packus_epi32:

x = _mm_and_si128(x, _mm_set1_epi32(65535));
y = _mm_and_si128(y, _mm_set1_epi32(65535));
x = _mm_packus_epi32(x, y);

Этот трюк работает, потому что он выполняет нулевое расширение 16-битных значений, что делает unsigned saturation no-op. Легче выполнить нулевое расширение, но вам нужно установить набор команд SSE4.1, чтобы сделать _mm_packus_epi32 доступным.

Можно упаковать 8 16-разрядных целых чисел, используя одну команду: _mm_perm_epi8. Но для этого требуется довольно редкий набор команд XOP.


И вот несколько слов о насыщенном преобразовании.

Фактически _mm_packus_epi32 intrinsic доступен, если вы меняете #include <xmmintrin.h> на #include <smmintrin.h> или #include <x86intrin.h>. Вам нужны как ваш процессор, так и компилятор для поддержки расширений SSE4.1.

Если у вас нет SSE4.1-совместимого процессора или компилятора или вы хотите, чтобы ваша программа была более переносимой, замените _mm_packus_epi32 на встроенный код следующим образом:

__m128i m1 = _mm_cmpgt_epi32(x, _mm_set1_epi32(0));
__m128i m2 = _mm_cmpgt_epi32(x, _mm_set1_epi32(65535));
x = _mm_and_si128(x, m1);
x = _mm_or_si128(x, m2);

Ответ 2

Я думаю, вы, вероятно, ищете инструкцию CVTTPS2DQ, для которой есть _mm_cvttps_epi32. См.: http://msdn.microsoft.com/en-us/library/c8c5hx3b(v=vs.71).aspx#vcref_mm_cvttps_epi32


Вот полная реализация, которая принимает 2 x SSE float-вектора и преобразует их в один упакованный 8 x 16-разрядный беззнаковый вектор с wraparound:

#include <stdio.h>
#include <tmmintrin.h>

__m128i vec_float_to_short(const __m128 v1, const __m128 v2)
{
    __m128i v1i = _mm_cvttps_epi32(v1);
    __m128i v2i = _mm_cvttps_epi32(v2);
    v1i = _mm_shuffle_epi8(v1i, _mm_setr_epi8(0, 1, 4, 5, 8, 9, 12, 13, 255, 255, 255, 255, 255, 255, 255, 255));
    v2i = _mm_shuffle_epi8(v2i, _mm_setr_epi8(255, 255, 255, 255, 255, 255, 255, 255, 0, 1, 4, 5, 8, 9, 12, 13));
    return _mm_or_si128(v1i, v2i);
}

int main(void)
{
    __m128 v1 = _mm_setr_ps(0.0f, 1.0f, -1.0f, 32767.0f);
    __m128 v2 = _mm_setr_ps(-32768.0f, 32768.0f, 99999.0f, -99999.0f);
    __m128i v3 = vec_float_to_short(v1, v2);

    printf("v1 = %vf\n", v1);
    printf("v2 = %vf\n", v2);
    printf("v3 = %vhu\n", v3);

    return 0;
}

Обратите внимание, что для этого используется PSHUFB (_mm_shuffle_epi8), для которого требуется SSSE3 aka SSE3.5 aka MNI (см. tmmintrin.h), так что это будет работать только на достаточно текущем CPU (что-нибудь от Intel за последние 6 лет или так).

$ gcc -Wall -mssse3 vec_float_to_short.c -o vec_float_to_short
$ ./vec_float_to_short 
v1 = 0.000000 1.000000 -1.000000 32767.000000
v2 = -32768.000000 32768.000000 99999.000000 -99999.000000
v3 = 0 1 65535 32767 32768 32768 34463 31073
$ 

Обратите внимание, что не все версии gcc поддерживают спецификатор формата printf v для векторов SIMD (я использую Apple gcc в OS X в этом случае).