【问题标题】:How to perform uint32/float conversion with SSE?如何使用 SSE 执行 uint32/float 转换?
【发布时间】:2016-03-08 01:56:11
【问题描述】:

在 SSE 中有一个函数 _mm_cvtepi32_ps(__m128i input),它接受 32 位宽有符号整数 (int32_t) 的输入向量并将它们转换为 floats。

现在,我想将输入整数解释为无符号。但是没有函数_mm_cvtepu32_ps,我找不到一个实现。你知道我在哪里可以找到这样的功能,或者至少给出实现的提示吗? 为了说明结果的差异:

unsigned int a = 2480160505; // 10010011 11010100 00111110 11111001   
float a1 = a; // 01001111 00010011 11010100 00111111;  
float a2 = (signed int)a; // 11001110 11011000 01010111 10000010

【问题讨论】:

  • 如果您使用的是 32 位 int,则最大正值为 2147483647,因此您的号码 2480160505 不能表示为 signed int
  • 我猜 OP 的意思是 2480160505U ?
  • 如果已知您的值在有限范围内(我认为 [0 .. 2^23],因为单精度有 23 位尾数),请尝试 Mysticial 的 64-bit unsigned to/from double 的 32 位版本。他很好地解释了该方法,您应该能够找到正确的常量来使 IEEE-754 位模式技巧起作用。

标签: c x86 sse simd


【解决方案1】:

使用 Paul R 的解决方案和我之前的解决方案 四舍五入的浮点数与原始整数的差小于或等于 0.75 ULP(最后一个单位)。在这些方法中 在两个地方可能会发生舍入:在 _mm_cvtepi32_ps 和 在_mm_add_ps。这会导致某些输入的结果尽可能不准确。

例如用 Paul R 的方法 0x2000003=33554435 转换为 33554432.0,但是 33554436.0 也作为浮点数存在,在这里会更好。 我之前的解决方案也存在类似的错误。 编译器生成的代码see here 也可能出现这种不准确的结果。

按照gcc(see Peter Cordes' answer to that other SO question)的方法,得到0.5 ULP以内的准确转换:

inline __m128 _mm_cvtepu32_ps(const __m128i v)
{
    __m128i msk_lo    = _mm_set1_epi32(0xFFFF);
    __m128  cnst65536f= _mm_set1_ps(65536.0f);

    __m128i v_lo      = _mm_and_si128(v,msk_lo);          /* extract the 16 lowest significant bits of v                                   */
    __m128i v_hi      = _mm_srli_epi32(v,16);             /* 16 most significant bits of v                                                 */
    __m128  v_lo_flt  = _mm_cvtepi32_ps(v_lo);            /* No rounding                                                                   */
    __m128  v_hi_flt  = _mm_cvtepi32_ps(v_hi);            /* No rounding                                                                   */
            v_hi_flt  = _mm_mul_ps(cnst65536f,v_hi_flt);  /* No rounding                                                                   */
    return              _mm_add_ps(v_hi_flt,v_lo_flt);    /* Rounding may occur here, mul and add may fuse to fma for haswell and newer    */
}                                                         /* _mm_add_ps is guaranteed to give results with an error of at most 0.5 ULP     */

请注意,只要_mm_cvt_ps 可以转换,其他高位/低位分区都是可能的 两件都浮动而不四舍五入。 例如,具有 20 个高位和 12 个低位的分区将同样有效。

【讨论】:

  • _mm_cvtepu32_ps 存在于 AVX512VL 指令集中。如果你的 CPU 有 AVX512 但你的编译器没有 _mm_cvtepu32_ps 那么你可以使用 _mm512_castps512_ps128(_mm512_cvtepu32_ps(_mm512_castsi128_si512(a)))
  • @AFog:好点。 AVX512/AVX512VL 确实填补了 SSE/AVX 指令集中的许多空白。您的_mm_cvtepu32_ps 解决方法对于没有 AVX512VL 的 Knights Landing 和 Knights Mill CPU 特别有用。
  • 更多赞成这个答案!这是唯一一个给出与 c++ 转换相同的答案的 exactly:f = float(v) 会。使用范围 [0, 0xffffffff] 内的所有输入进行测试。
【解决方案2】:

我认为 Paul 的回答很好,但对于 v=4294967295U (=2^32-1) 却失败了。在这种情况下,v2=2^31-1 和 v1=2^31。内在的 _mm_cvtepi32_ps 将 2^31 转换为 -2.14748365E9 。 v2=2^31-1 被转换为 2.14748365E9,因此_mm_add_ps 返回 0(由于四舍五入,v1f 和 v2f 彼此完全相反)。

以下解决方案的想法是将 v 的最高有效位复制到 v_high。 v 的其他位被复制到 v_low。 v_high 转换为 0 或 2.14748365E9 。

inline __m128 _mm_cvtepu32_v3_ps(const __m128i v)
{
__m128i msk0=_mm_set1_epi32(0x7FFFFFFF);
__m128i zero=_mm_xor_si128(msk0,msk0);
__m128i cnst2_31=_mm_set1_epi32(0x4F000000); /* IEEE representation of float 2^31 */

__m128i v_high=_mm_andnot_si128(msk0,v);
__m128i v_low=_mm_and_si128(msk0,v);
__m128  v_lowf=_mm_cvtepi32_ps(v_low);
__m128i msk1=_mm_cmpeq_epi32(v_high,zero);
__m128  v_highf=_mm_castsi128_ps(_mm_andnot_si128(msk1,cnst2_31));  
__m128  v_sum=_mm_add_ps(v_lowf,v_highf);
return v_sum;

}


更新

可以减少指令的数量:

inline __m128 _mm_cvtepu32_v4_ps(const __m128i v)
{
__m128i msk0=_mm_set1_epi32(0x7FFFFFFF);
__m128i cnst2_31=_mm_set1_epi32(0x4F000000);

__m128i msk1=_mm_srai_epi32(v,31);
__m128i v_low=_mm_and_si128(msk0,v);
__m128  v_lowf=_mm_cvtepi32_ps(v_low);
__m128  v_highf=_mm_castsi128_ps(_mm_and_si128(msk1,cnst2_31));  
__m128  v_sum=_mm_add_ps(v_lowf,v_highf);
return v_sum;
}

内在的_mm_srai_epi32 将 v 的最高有效位向右移动,同时将符号位移动,这在这里非常有用。

【讨论】:

  • 很好 - 我没有考虑过 UINT_MAX 的边界情况 - 我想知道是否有更有效的方法来做到这一点?
  • @Paul R:似乎确实有一种更有效的方法,请参阅更新。谢谢你的评论。
  • 由于累积舍入误差,此解决方案在少数情况下略有不准确。 Wim 的解决方案更好。
【解决方案3】:

此功能存在于 AVX-512 中,但如果您不能等到那时,我唯一可以建议的是将 unsigned int 输入值转换为较小的值对,转换它们,然后再次将它们加在一起,例如

inline __m128 _mm_cvtepu32_ps(const __m128i v)
{
    __m128i v2 = _mm_srli_epi32(v, 1);     // v2 = v / 2
    __m128i v1 = _mm_sub_epi32(v, v2);     // v1 = v - (v / 2)
    __m128 v2f = _mm_cvtepi32_ps(v2);
    __m128 v1f = _mm_cvtepi32_ps(v1);
    return _mm_add_ps(v2f, v1f); 
}

更新

正如@wimhis answer 中所指出的,上述解决方案对于UINT_MAX 的输入值失败。这是一个更强大但效率稍低的解决方案,它应该适用于整个uint32_t 输入范围:

inline __m128 _mm_cvtepu32_ps(const __m128i v)
{
    __m128i v2 = _mm_srli_epi32(v, 1);                 // v2 = v / 2
    __m128i v1 = _mm_and_si128(v, _mm_set1_epi32(1));  // v1 = v & 1
    __m128 v2f = _mm_cvtepi32_ps(v2);
    __m128 v1f = _mm_cvtepi32_ps(v1);
    return _mm_add_ps(_mm_add_ps(v2f, v2f), v1f);      // return 2 * v2 + v1
}

【讨论】:

  • 谢谢。顺便说一句,为什么不在例程参数中使用引用 val?
  • 您的意思是将v 传递给_mm_cvtepu32_ps 作为C++ 引用或通过C 指针或其他东西,而不是通过值?实际上,只要函数是inline,它就没有任何区别。
  • @KirillLykov:对于没有内联但具有向量参数的函数的罕见情况,按值传递更有效。向量不是在整数寄存器中传递指针,而是在向量寄存器中传递。所以它不必被调用者溢出并由被调用者重新加载。 (实际上,在 SysV ABI 中,如果调用者想要保留旧值,则必须将其溢出,因为该 ABI 中没有保留调用的向量 reg。尽管如此,它可以使被调用函数免于加载它.)
  • 呵呵,我想问一下,先零扩展为 64 位,然后将两次转换的结果重新洗牌成浮点向量是否会更好,但是没有任何函数可以转换FP 到/从除签名 DWORD 之外的任何东西。 (直到 AVX512。整洁。)
猜你喜欢
  • 2017-05-11
  • 2013-05-30
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-11-16
  • 2014-06-28
  • 1970-01-01
相关资源
最近更新 更多