SSE 内在函数:将 32 位浮点数转换为 UNSIGNED 8 位整数

Posted

技术标签:

【中文标题】SSE 内在函数:将 32 位浮点数转换为 UNSIGNED 8 位整数【英文标题】:SSE intrinsics: Convert 32-bit floats to UNSIGNED 8-bit integers 【发布时间】:2015-04-24 19:35:16 【问题描述】:

使用 SSE 内在函数,我得到了一个包含四个 32 位浮点数的向量,这些浮点数被限制在 0-255 的范围内并四舍五入到最接近的整数。我现在想把这四个写成字节。

有一个内在的_mm_cvtps_pi8 会将 32 位转换为 8 位 signed 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;

UPDATE2:根据 Harold 的回答,这是一个更好的解决方案:

#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;

【问题讨论】:

等等,你知道_mm_shuffle_pi8 是mm-register 版本,对吧?不要忘记您的_mm_empty @harold:哦,好点。但是,我在编译器命令行上有-mfpmath=sse 我可以建议将_mm_packus_epi32 替换为_mm_packs_epi32 吗?正如彼得所说,它工作得很好,只需要 SSE2。您的(基于 harold 的)需要 SSE4.1 【参考方案1】:

没有从浮点到字节的直接转换,_mm_cvtps_pi8 是一个复合。 _mm_cvtps_pi16 也是一个组合,在这种情况下,它只是在做一些无意义的事情,你可以用 shuffle 撤消这些事情。他们还返回烦人的__m64's。

无论如何,我们可以转换为双字(有符号,但这没关系),然后将它们打包(无符号)或洗牌成字节。 _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

【讨论】:

我真的很喜欢你的包装解决方案。好的是舍入和夹紧是自动发生的。然而,有一个极端情况,尽管我认为它不会影响我:如果我第一次将 100000 放入其中一个浮点数中,它会被限制为 65535(我假设)。然而,第二次它被重新解释为有符号值 (-1),然后被 packuswb 钳制为零。有什么低成本的解决办法吗? @TimothyMiller 也许吧,我真的想不出什么聪明的东西,只是显而易见的“pminuw with 255” @TimothyMiller:是的,packuswb 将其输入视为有符号,但输出为无符号,因此存在问题。您可以使用pand 屏蔽packusdwpackuswb 之间的偶数字节,以达到与pminuw 相同的结果。或者使用 [-128..127] 范围内的浮点数,并使用 paddb 128 秒的向量将它们转换为 [0..255] 范围。 我想我解决了这个问题:只需使用packssdw 作为第一步,因为这就是packuswb 将如何解释它。我补充说作为答案。我觉得我一定错过了一些东西,否则当我为***.com/questions/32284106/…写一个答案时,我上次在寻找时没有想到这一点而感到很愚蠢@ 需要注意的是packusdw需要SSE4(AMD的SSE4a不支持)。【参考方案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's how packuswb treats its input
packuswb xmm0, xmm1       ; 16 uint8_t
movdqa   [rdi], xmm0

这仅需要 SSE2,packusdw 不需要 SSE4.1。

我认为这就是 SSE2 仅包含从 dword 到 word 的签名包,但包含从 word 到 byte 的有符号和无符号包的原因。 packuswd 仅在您的最终目标是 uint16_t 时才有用,而不是进一步打包。 (从那时起,您需要在将其送入下一个包之前屏蔽符号位)。

如果您确实使用了packusdw -&gt; packuswb,那么当第一步饱和到uint16_t > 0x7fff 时,您会得到虚假结果。 packuswb 会将其解释为负数 int16_t 并将其饱和到 0。packssdw 会将此类输入饱和到 0x7fff,最大值 int16_t

(如果您的 32 位输入始终 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,pshufbpack* 仅是端口 5。

【讨论】:

在我的情况下,我得到了负值,所以哈罗德的洗牌是不够的。您的 shuffle 有效,但不幸的是因为 pmaxsd 而需要 SSE4.1。 SSE4.1 解决方案(packs 和 suffle)在我的 i7 980x 上以相同的速度运行。现在将尝试您的第一个解决方案。 您的第一个建议,使用 packssdw,效果很好(与 harold 一起使用)。现在我们得到了 SSE2 和 SSE4.1! (两者也以相同的速度运行)

以上是关于SSE 内在函数:将 32 位浮点数转换为 UNSIGNED 8 位整数的主要内容,如果未能解决你的问题,请参考以下文章

你如何使用霓虹内在函数加载 3 个浮点数

将一个 32 位浮点数转换为两个 16 位 uint 数,然后再次转换回该 32 位浮点数

将 32 位浮点数转换为 16 位 PCM 范围

使用内在函数将双 SSE2/AVX/AVX512 存储为浮点数的最佳方法

用脚编码音频:将 32 位浮点数转换为 mp3

用于灰度到 ARGB 转换的 C++ SSE2 或 AVX2 内在函数