Как выполнить преобразование uint32/float с помощью 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

Ответ 1

Эта функция существует в 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
}

Ответ 2

Я думаю, что ответ Павла хороший, но он не подходит для 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;

}



Update

Можно было сократить количество инструкций:

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 вправо, а смещение знаковых бит, что оказывается здесь весьма полезным.

Ответ 3

С решением Paul R и моим предыдущим решением разница между округленной плавающей точкой и исходным целым меньше или равна 0,75 ULP (единица в последнем месте). В этих методах в двух местах может происходить округление: в _mm_cvtepi32_ps и в _mm_add_ps. Это приводит к результатам, которые не так точны для некоторых входов.

Например, метод Paul R 0x2000003 = 33554435 преобразуется в 33554432.0, но 33554436.0 также существует как плавающий, что было бы лучше здесь. Мое предыдущее решение страдает от подобных неточностей. Такие неточные результаты могут также возникать с кодом, сгенерированным компилятором, см. Здесь.

Следуя подходу gcc (см. ответ Питера Кордеса на этот другой вопрос 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 младшими битами будет работать одинаково хорошо.