将浮点向量转换为 16 位 int 而不饱和

Posted

技术标签:

【中文标题】将浮点向量转换为 16 位 int 而不饱和【英文标题】:Converting float vector to 16-bit int without saturating 【发布时间】:2012-08-25 03:23:45 【问题描述】:

我想将浮点值转换为不饱和的 16 位无符号整数(改为环绕/溢出)。

#include <iostream>
#include <xmmintrin.h>

void satur_wrap()

    const float bigVal = 99000.f;
    const __m128 bigValVec = _mm_set1_ps(bigVal);

    const __m64 outVec64 =_mm_cvtps_pi16(bigValVec);

#if 0
    const __m128i outVec = _mm_movpi64_epi64(outVec64);
#else

    #if 1
        const __m128i outVec  = _mm_packs_epi32(_mm_cvttps_epi32(bigValVec), _mm_cvttps_epi32(bigValVec));
    #else
        const __m128i outVec  = _mm_cvttps_epi32(bigValVec);
    #endif

#endif

    uint16_t *outVals = NULL;
    posix_memalign((void **) &outVals, sizeof(__m128i), sizeof(__m128i));

    _mm_store_si128(reinterpret_cast<__m128i *>(outVals), outVec);

    for (int i = 0; i < sizeof(outVec) / sizeof(*outVals); i++)
    
        std::cout << "outVals[" << i << "]: " << outVals[i] << std::endl;
    

    std::cout << std::endl
        << "\tbigVal: " << bigVal << std::endl
        << "\t(unsigned short) bigVal: " << ((unsigned short) bigVal)  << std::endl
        << "\t((unsigned short)((int) bigVal)): " << ((unsigned short)((int) bigVal)) << std::endl
        << std::endl;

示例执行:

$ ./row
outVals[0]: 32767
outVals[1]: 32767
outVals[2]: 32767
outVals[3]: 32767
outVals[4]: 32767
outVals[5]: 32767
outVals[6]: 32767
outVals[7]: 32767

        bigVal: 99000
        (unsigned short) bigVal: 65535
        ((unsigned short)((int) bigVal)): 33464

((unsigned short)((int) bigVal)) 表达式可以按需要工作(但它可能是 UB,对吗?)。但我找不到与 SSE 非常相似的东西。我一定遗漏了什么,但我找不到将四个 32 位 floats 转换为四个 32 位 ints 的原语。


编辑:糟糕,我认为 32 位整数 -> 16 位无符号整数转换使用环绕是“正常的”。但我后来了解到_mm_packs_epi32 使用有符号饱和(而且似乎没有_mm_packus_epi32)。除了_mm_packus_epi32之外,有没有办法设置模式或其他原语?

【问题讨论】:

顺便说一下,_mm_store_si128 可能无法正常工作,因为 outVals 可能不是 16 字节对齐的。将_mm_storeu_si128 用于未处理负载或注意outVals 的正确对齐。 是的,抱歉,在我的示例中忘记了 posix_memalign。 见:CVTTPS2DQ又名_mm_cvttps_epi32 嘿@PaulR,看起来它会完美运行!让它成为一个答案,我会选择它。 @Brian:没问题 - 评论现在转换为答案。 【参考方案1】:

我认为您可能正在寻找CVTTPS2DQ 指令,其内在函数是_mm_cvttps_epi32。见:http://msdn.microsoft.com/en-us/library/c8c5hx3b(v=vs.71).aspx#vcref_mm_cvttps_epi32


这是一个完整的实现,它采用 2 x SSE 浮点向量并将它们转换为单个打包的 8 x 16 位无符号向量,并带有环绕:

#include <stdio.h>
#include <tmmintrin.h>

__m128i vec_float_to_short(const __m128 v1, const __m128 v2)

    __m128i v1i = _mm_cvttps_epi32(v1);
    __m128i v2i = _mm_cvttps_epi32(v2);
    v1i = _mm_shuffle_epi8(v1i, _mm_setr_epi8(0, 1, 4, 5, 8, 9, 12, 13, 255, 255, 255, 255, 255, 255, 255, 255));
    v2i = _mm_shuffle_epi8(v2i, _mm_setr_epi8(255, 255, 255, 255, 255, 255, 255, 255, 0, 1, 4, 5, 8, 9, 12, 13));
    return _mm_or_si128(v1i, v2i);


int main(void)

    __m128 v1 = _mm_setr_ps(0.0f, 1.0f, -1.0f, 32767.0f);
    __m128 v2 = _mm_setr_ps(-32768.0f, 32768.0f, 99999.0f, -99999.0f);
    __m128i v3 = vec_float_to_short(v1, v2);

    printf("v1 = %vf\n", v1);
    printf("v2 = %vf\n", v2);
    printf("v3 = %vhu\n", v3);

    return 0;

请注意,这使用了PSHUFB (_mm_shuffle_epi8),它需要 SSSE3 aka SSE3.5 aka MNI(请参阅tmmintrin.h),因此这只适用于当前合理的 CPU(过去 6 年中来自 Intel 的任何 CPU)左右)。

$ gcc -Wall -mssse3 vec_float_to_short.c -o vec_float_to_short
$ ./vec_float_to_short 
v1 = 0.000000 1.000000 -1.000000 32767.000000
v2 = -32768.000000 32768.000000 99999.000000 -99999.000000
v3 = 0 1 65535 32767 32768 32768 34463 31073
$ 

请注意,并非所有版本的 gcc 都支持用于 SIMD 向量的 printf v 格式说明符(在本例中,我在 OS X 上使用 Apple 的 gcc)。

【讨论】:

【参考方案2】:

我只回答有关 32 位整数 -> 16 位无符号整数转换的部分问题。

由于您需要回绕,只需取每个包含 32 位整数的双字的低位字。这些 16 位整数与 16 位未使用数据交错,因此将它们打包成一个连续数组可能会很方便。最简单的方法是使用_mm_shuffle_epi8 intrinsic (SSSE3)。

如果您希望您的程序更具可移植性并且只需要 SSE2 指令集,您可以使用 _mm_packs_epi32 打包值,但使用以下技巧禁用其饱和行为:

x = _mm_slli_epi32(x, 16);
y = _mm_slli_epi32(y, 16);

x = _mm_srai_epi32(x, 16);
y = _mm_srai_epi32(y, 16);

x = _mm_packs_epi32(x, y);

这个技巧之所以有效,是因为它执行 16 位值的符号扩展,这使得有符号饱和成为空操作。

同样的技巧适用于_mm_packus_epi32:

x = _mm_and_si128(x, _mm_set1_epi32(65535));
y = _mm_and_si128(y, _mm_set1_epi32(65535));
x = _mm_packus_epi32(x, y);

这个技巧之所以有效,是因为它执行 16 位值的零扩展,这使得无符号饱和成为无操作。执行零扩展更容易,但您需要 SSE4.1 指令集才能使_mm_packus_epi32 可用。

可以使用一条指令打包 8 个 16 位整数:_mm_perm_epi8。但这需要非常少见的 XOP 指令集。


这里有几个关于饱和转换的词。

事实上,如果您将#include &lt;xmmintrin.h&gt; 更改为#include &lt;smmintrin.h&gt;#include &lt;x86intrin.h&gt;,则可以使用_mm_packus_epi32 内在函数。您需要 CPU 和编译器来支持 SSE4.1 扩展。

如果您没有与 SSE4.1 兼容的 CPU 或编译器,或者希望您的程序更具可移植性,请将 _mm_packus_epi32 内部代码替换为如下代码:

__m128i m1 = _mm_cmpgt_epi32(x, _mm_set1_epi32(0));
__m128i m2 = _mm_cmpgt_epi32(x, _mm_set1_epi32(65535));
x = _mm_and_si128(x, m1);
x = _mm_or_si128(x, m2);

【讨论】:

但我不想饱和。我想要((unsigned short)((int) bigVal)) 表达式中的溢出/环绕。你的例子会饱和,对吧? 我可能不清楚要求_mm_packus_epi32,这无关或至少不是主要问题。不过不知道&lt;smmintrin.h&gt;。 +1。 如果需要回绕,只需取每个包含 32 位整数的双字的低位字即可。您可以使用 _mm_shuffle_epi8 intrinsic (SSSE3) 来准备这些 16 位整数的连续数组。但是在这种情况下没有什么可以转换的。 好吧,哇,看起来很值得。这将需要更多的 RTFMing 和实验,我们会尽快回复您。

以上是关于将浮点向量转换为 16 位 int 而不饱和的主要内容,如果未能解决你的问题,请参考以下文章

使用按位运算将 Int 转换为 Float 或将 Float 转换为 Int(软件浮点)

将 32 位浮点音频转换为 16 位字节数组?

unsigned int 向量的差异,解释为有符号并转换为未正确呈现的浮点向量

如何将 Int16 音频样本的数据转换为浮点音频样本数组

将32位浮点音频转换为16位

何时使用 trunc() 而不是 int() 将浮点类型数转换为整数更好?