Преобразование вектора 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 в этом случае).