如何使用 SSE 进行 uint32/float 转换?
How to perform uint32/float conversion with SSE?
在 SSE 中有一个函数 _mm_cvtepi32_ps(__m128i input)
,它接受 32 位宽有符号整数 (int32_t
) 的输入向量并将它们转换为 float
s。
现在,我想将输入整数解释为未签名。但是没有函数 _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
此功能存在于 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);
}
更新
如 @wim in 所述,上述解决方案对于输入值 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
}
我认为 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
returns 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;
}
Intrinsic _mm_srai_epi32
将 v 的最高有效位右移,同时移入符号位,这在这里非常有用。
使用 Paul R 的解决方案和我之前的解决方案
四舍五入后的浮点数与原整数之差小于等于
0.75 ULP(排在最后的单位)。在这些方法中
在两个地方可能会发生舍入:在 _mm_cvtepi32_ps 和
在 _mm_add_ps。这导致某些输入的结果不是尽可能准确。
例如,使用 Paul R 的方法 0x2000003=33554435 被转换为 33554432.0,但 33554436.0
也作为浮点数存在,在这里会更好。
我以前的解决方案也存在类似的错误。
这种不准确的结果也可能出现在编译器生成的代码中,。
按照gcc的方法,得到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 */
注意其他高bits/low位分区也是可以的只要_mm_cvt_ps能转换
两件都浮动而不四舍五入。
例如,具有 20 个高位和 12 个低位的分区将同样有效。
在 SSE 中有一个函数 _mm_cvtepi32_ps(__m128i input)
,它接受 32 位宽有符号整数 (int32_t
) 的输入向量并将它们转换为 float
s。
现在,我想将输入整数解释为未签名。但是没有函数 _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
此功能存在于 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);
}
更新
如 @wim in 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
}
我认为 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
returns 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;
}
Intrinsic _mm_srai_epi32
将 v 的最高有效位右移,同时移入符号位,这在这里非常有用。
使用 Paul R 的解决方案和我之前的解决方案 四舍五入后的浮点数与原整数之差小于等于 0.75 ULP(排在最后的单位)。在这些方法中 在两个地方可能会发生舍入:在 _mm_cvtepi32_ps 和 在 _mm_add_ps。这导致某些输入的结果不是尽可能准确。
例如,使用 Paul R 的方法 0x2000003=33554435 被转换为 33554432.0,但 33554436.0
也作为浮点数存在,在这里会更好。
我以前的解决方案也存在类似的错误。
这种不准确的结果也可能出现在编译器生成的代码中,
按照gcc的方法
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 */
注意其他高bits/low位分区也是可以的只要_mm_cvt_ps能转换 两件都浮动而不四舍五入。 例如,具有 20 个高位和 12 个低位的分区将同样有效。