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.