如何使用SSE执行uint32 / float转换?

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

现在,我想将输入整数解释为未签名。 但是没有函数_mm_cvtepu32_ps ,我找不到一个实现。 你知道我在哪里可以找到这样的function,或者至少对实现有所暗示吗? 为了说明结果的差异:

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

此function存在于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); } 

UPDATE

正如@wim在他的回答中所指出的,上述解决方案对于输入值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 } 

我认为保罗的答案很好,但是对于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的最高有效位向右移位,同时移位符号位,这在这里非常有用。

使用Paul R的解决方案和我之前的解决方案,圆形浮点和原始整数之间的差异小于或等于0.75 ULP(最后位置的单位)。 在这些方法中,可能会在两个位置进行舍入:_mm_cvtepi32_ps和_mm_add_ps。 这导致某些输入的结果不尽可能准确。

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

遵循gcc的方法(参见Peter Cordes对其他SO问题的回答) ,获得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个低位的分区将同样有效。