Вътрешни SSE: Преобразувайте 32-битови числа с плаваща замък в 8-битови цели числа без ПОДПИС

Използвайки SSE intrinsics, получих вектор от четири 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-register, нали? Не забравяйте своя _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)


Няма директно преобразуване от float в байт, _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

Използване на разбъркване: (не е тествано)

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
Наистина ми харесва вашето решение за опаковката. Хубавото е, че закръгляването И затягането стават автоматично. Има обаче един ъглов случай, въпреки че не мисля, че ме засяга: ако сложа, да речем, 100 000 в един от поплавъците, първия път, той се затяга на 65 535 (предполагам). Вторият път обаче се интерпретира отново като стойност със знак (-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 вектор от 128s. - person Peter Cordes; 30.08.2015
comment
Мисля, че реших проблема: просто използвайте packssdw като първа стъпка, защото така packuswb ще го интерпретира. Добавих това като отговор. Имам чувството, че трябва да пропускам нещо, или пък се чувствам тъпо, че не се сетих за последния път, когато търсих, когато написах отговор за stackoverflow.com/questions/32284106/ - person Peter Cordes; 04.12.2015
comment
Трябва да се отбележи, че packusdw изисква SSE4 (SSE4a на AMD не го поддържа). - person zett42; 16.05.2017
comment
Съгласен съм с zett42. Но замяната му с packssdw (както Питър предложи) изглежда работи добре и ни връща към SSE2. - person user1593842; 08.09.2017

Можем да разрешим проблема със затягането без знак, като направим първия етап на опаковане с насищане със знак. [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's how packuswb treats its input
packuswb xmm0, xmm1       ; 16 uint8_t
movdqa   [rdi], xmm0

Това изисква само SSE2, а не SSE4.1 за packusdw.

Предполагам, че това е причината SSE2 да включва само пакет със знак от dword към дума, но както подписан, така и неподписан пакет от дума към байт. 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 (packs и suffle) работят с еднаква скорост на моя i7 980x. Ще опитате първото си решение сега. - person user1593842; 08.09.2017
comment
Първото ви предложение, използвайки packssdw, работи страхотно (използвах го с harold's). Сега имаме SSE2 и SSE4.1! (и двете работят с еднаква скорост) - person user1593842; 08.09.2017