Встроенные функции SSE: преобразование 32-разрядных чисел с плавающей запятой в 8-разрядные целые числа без знака.

Используя встроенные функции SSE, я получил вектор из четырех 32-битных чисел с плавающей запятой, ограниченный диапазоном 0–255 и округленный до ближайшего целого числа. Теперь я хотел бы записать эти четыре байта.

Существует внутренний _mm_cvtps_pi8, который преобразует 32-битное в 8-битное подписанное int, но проблема в том, что любое значение, превышающее 127, ограничивается до 127. Я не могу найти никаких инструкций, которые будут зажимать к беззнаковым 8-битным значениям.

У меня есть интуиция, что я могу захотеть сделать какую-то комбинацию _mm_cvtps_pi16 и _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;
}

person Timothy Miller    schedule 24.04.2015    source источник
comment
Подождите, вы же знаете, _mm_shuffle_pi8 это версия с регистром мм, верно? Не забывай свой _mm_empty   -  person harold    schedule 24.04.2015
comment
@harold: О, хороший момент. Однако у меня -mfpmath=sse в командной строке компилятора.   -  person Timothy Miller    schedule 24.04.2015
comment
Могу я предложить заменить это _mm_packus_epi32 на _mm_packs_epi32? Как сказал Питер, он отлично работает и требует только SSE2. Ваш (на основе Гарольда) требует SSE4.1   -  person user1593842    schedule 08.09.2017


Ответы (2)


Прямого преобразования из числа с плавающей запятой в байтовое нет, _mm_cvtps_pi8 является составным. _mm_cvtps_pi16 также является составным, и в этом случае он просто выполняет какие-то бессмысленные вещи, которые вы отменяете с помощью перемешивания. Они также возвращают надоедливые __m64.

В любом случае, мы можем преобразовать в двойные слова (подписанные, но это не имеет значения), а затем упаковать (без знака) или перемешать их в байты. _mm_shuffle_(e)pi8 генерирует pshufb, 45-нм процессоры Core2 и 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

Использование перемешивания: (не проверено)

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
person harold    schedule 24.04.2015
comment
Мне очень нравится твоя упаковка. Что приятно, так это то, что округление и зажим происходят автоматически. Однако есть один угловой случай, хотя я не думаю, что он влияет на меня: если я помещаю, скажем, 100000 в один из поплавков, в первый раз он фиксируется на уровне 65535 (я предполагаю). Однако во второй раз оно интерпретируется заново как значение со знаком (-1), а затем ограничивается до нуля функцией packuswb. Какое-нибудь недорогое исправление для этого? - person Timothy Miller; 24.04.2015
comment
@TimothyMiller, может быть, я не могу придумать ничего умного, просто очевидное pminuw с 255 - person harold; 24.04.2015
comment
@TimothyMiller: Да, packuswb рассматривает ввод как подписанный, а вывод как беззнаковый, так что есть проблема. Вы можете использовать pand, чтобы замаскировать четные байты между packusdw и packuswb, чтобы получить тот же результат, что и pminuw. Или работайте с числами с плавающей запятой в диапазоне [-128..127] и преобразуйте их в диапазон [0..255] с paddb вектором 128 сек. - person Peter Cordes; 30.08.2015
comment
Думаю, я решил проблему: просто используйте packssdw в качестве первого шага, потому что packuswb будет интерпретировать это именно так. Я добавил это в качестве ответа. Я чувствую, что мне что-то не хватает, или я чувствую себя глупо из-за того, что не подумал об этом в последний раз, когда я искал, когда я написал ответ для stackoverflow.com/questions/32284106/ - person Peter Cordes; 04.12.2015
comment
Следует отметить, что packusdw требует SSE4 (AMD SSE4a его не поддерживает). - person zett42; 16.05.2017
comment
Я согласен с zett42. Но замена его на packssdw (как предложил Питер), похоже, работает нормально и возвращает нас к SSE2. - person user1593842; 08.09.2017

Мы можем решить проблему беззнакового зажима, выполнив первый этап упаковки с подписанным насыщением. [0-255] вписывается в 16-битное целое число со знаком, поэтому значения в этом диапазоне останутся незафиксированными. Значения вне этого диапазона останутся на той же стороне от него. Таким образом, шаг 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's 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, максимального int16_t.

(Если ваши 32-битные входные данные всегда ‹= 0x7fff, вы можете использовать любой, но SSE4.1 packusdw < / a> занимает больше байтов инструкций, чем SSE2 packsswd, и никогда не работает быстрее.)


Если ваши исходные значения не могут быть отрицательными, и у вас есть только один вектор из 4 чисел с плавающей запятой, а не много, вы можете использовать идею Гарольда 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.

person Peter Cordes    schedule 03.12.2015
comment
В моем случае я получил отрицательные значения, поэтому перетасовки Гарольда было недостаточно. Ваше перемешивание работает, но, к сожалению, требует SSE4.1 из-за pmaxsd. Оба решения SSE4.1 (пакеты и суффл) работают с одинаковой скоростью на моем i7 980x. Попробую ваше первое решение прямо сейчас. - person user1593842; 08.09.2017
comment
Ваше первое предложение, используя packssdw, отлично работает (использовал его с Гарольдом). Теперь у нас есть SSE2 и SSE4.1! (оба работают с одинаковой скоростью) - person user1593842; 08.09.2017