SSE intrinsics: конвертировать 32-битные поплавки в UNSIGNED 8-битные целые числа
Используя встроенные функции SSE, я получил вектор из четырех 32-разрядных чисел с ограничением в диапазоне 0-255 и округленный до ближайшего целого числа. Теперь я хотел бы записать эти четыре в байтах.
Существует встроенная _mm_cvtps_pi8
, которая преобразует 32-разрядное в 8-разрядное знаковое целое, но проблема в том, что любое значение, превышающее 127, оказывается ограниченным до 127. Я не могу найти никаких инструкций, которые ограничивают 8-разрядные значения без знака.
У меня есть интуиция, что я могу захотеть сделать комбинацию _mm_cvtps_pi16
и _mm_shuffle_pi8
последующей инструкцией перемещения, чтобы получить четыре байта, которые мне _mm_shuffle_pi8
в память. Это лучший способ сделать это? Я собираюсь выяснить, смогу ли я выяснить, как кодировать маску управления тасованием.
ОБНОВЛЕНИЕ: следующее, кажется, делает именно то, что я хочу. Есть ли способ лучше?
#include <tmmintrin.h>
#include <stdio.h>
unsigned char out[8];
unsigned char shuf[8] = { 0, 2, 4, 6, 128, 128, 128, 128 };
float ins[4] = {500, 0, 120, 240};
int main()
{
__m128 x = _mm_load_ps(ins); // Load the floats
__m64 y = _mm_cvtps_pi16(x); // Convert them to 16-bit ints
__m64 sh = *(__m64*)shuf; // Get the shuffle mask into a register
y = _mm_shuffle_pi8(y, sh); // Shuffle the lower byte of each into the first four bytes
*(int*)out = _mm_cvtsi64_si32(y); // Store the lower 32 bits
printf("%d\n", out[0]);
printf("%d\n", out[1]);
printf("%d\n", out[2]);
printf("%d\n", out[3]);
return 0;
}
ОБНОВЛЕНИЕ 2: Здесь еще лучшее решение, основанное на ответе Гарольда:
#include <smmintrin.h>
#include <stdio.h>
unsigned char out[8];
float ins[4] = {10.4, 10.6, 120, 100000};
int main()
{
__m128 x = _mm_load_ps(ins); // Load the floats
__m128i y = _mm_cvtps_epi32(x); // Convert them to 32-bit ints
y = _mm_packus_epi32(y, y); // Pack down to 16 bits
y = _mm_packus_epi16(y, y); // Pack down to 8 bits
*(int*)out = _mm_cvtsi128_si32(y); // Store the lower 32 bits
printf("%d\n", out[0]);
printf("%d\n", out[1]);
printf("%d\n", out[2]);
printf("%d\n", out[3]);
return 0;
}
Ответы
Ответ 1
Нет прямого преобразования от float to byte, _mm_cvtps_pi8
является составным. _mm_cvtps_pi16
также является составным, и в этом случае он просто делает некоторые бессмысленные вещи, которые вы отмените с тасованием. Они также возвращают раздражающие __m64
.
В любом случае, мы можем конвертировать в dwords (подписанный, но это не имеет значения), а затем упаковать (без знака) или перетасовать их в байты. _mm_shuffle_(e)pi8
генерирует процессоры pshufb
, Core2 45nm и AMD не слишком любят его, и вы должны где-то получить маску.
В любом случае вам не нужно сначала округлить до ближайшего целого числа, конверт сделает это. По крайней мере, если вы не испортили режим округления.
Использование пакетов 1: (не проверено) - возможно, не полезно, packusdw
уже выводит неподписанные слова, но затем packuswb
хочет снова записать подписанные слова. Удерживается, потому что он упоминается в другом месте.
cvtps2dq xmm0, xmm0
packusdw xmm0, xmm0 ; unsafe: saturates to a different range than packuswb accepts
packuswb xmm0, xmm0
movd somewhere, xmm0
Использование разных тасов:
cvtps2dq xmm0, xmm0
packssdw xmm0, xmm0 ; correct: signed saturation on first step to feed packuswb
packuswb xmm0, xmm0
movd somewhere, xmm0
Использование shuffle: (не проверено)
cvtps2dq xmm0, xmm0
pshufb xmm0, [shufmask]
movd somewhere, xmm0
shufmask: db 0, 4, 8, 12, 80h, 80h, 80h, 80h, 80h, 80h, 80h, 80h, 80h, 80h, 80h, 80h
Ответ 2
Мы можем решить неподписанную проблему зажима, выполнив первый этап упаковки с подписанной насыщенностью. [0-255]
подходит к подписанному 16-битовому int, поэтому значения в этом диапазоне будут оставаться незамкнутыми. Значения вне этого диапазона останутся на одной стороне. Таким образом, шаг signed16 → unsigned8 будет зажимать их правильно.
;; SSE2: good for arrays of inputs
cvtps2dq xmm0, [rsi] ; 4 floats
cvtps2dq xmm1, [rsi+16] ; 4 more floats
packssdw xmm0, xmm1 ; 8 int16_t
cvtps2dq xmm1, [rsi+32]
cvtps2dq xmm2, [rsi+48]
packssdw xmm1, xmm2 ; 8 more int16_t
; signed because that how packuswb treats its input
packuswb xmm0, xmm1 ; 16 uint8_t
movdqa [rdi], xmm0
Для этого требуется только SSE2, а не SSE4.1 для packusdw
.
Я предполагаю, что это причина, по которой SSE2 включал только подписанный пакет от слова к слову, но как подписанный, так и неподписанный пакет от слова к байту. packuswd
полезен, если ваша конечная цель uint16_t
, а не дальнейшая упаковка. (С тех пор вам нужно будет скрыть знаковый бит, прежде чем подавать его на следующий пакет).
Если вы использовали packusdw -> packuswb
, вы получили бы фиктивные результаты, когда первый шаг был насыщен до uint16_t
> 0x7fff. packuswb
интерпретирует это как отрицательный int16_t
и насыщает его до 0. packssdw
будет насыщать такие входы до 0x7fff
, max int16_t
.
(Если ваши 32-битные входы всегда <= 0x7fff, вы можете использовать их, но SSE4.1 packusdw
принимает больше байтов команд, чем SSE2 packsswd
и никогда не работает быстрее.)
Если ваши исходные значения не могут быть отрицательными, и у вас есть только один вектор из 4 поплавков, не так много, вы можете использовать идею harold pshufb
. Если нет, вам нужно закрепить отрицательные значения до нуля, а не обрезать их, перетасовывая низкие байты на место.
Используя
;; SSE4.1, good for a single vector. Use the PACK version above for arrays
cvtps2dq xmm0, xmm0
pmaxsd xmm0, zeroed-register
pshufb xmm0, [mask]
movd [somewhere], xmm0
может быть немного более эффективным, чем использование двух инструкций pack
, поскольку pmax
может работать на порту 1 или 5 (Intel Haswell). cvtps2dq
- только порт 1, pshufb
и pack*
- только порт 5.