Наиболее эффективный стандартно-совместимый способ интерпретации int как float

Предположим, у меня есть гарантии, что float является IEEE 754 binary32. Учитывая битовый шаблон, соответствующий допустимому float, сохраненному в std::uint32_t, как это можно интерпретировать как float в самом эффективном стандартном уступчивом пути?

float reinterpret_as_float(std::uint32_t ui) {
   return /* apply sorcery to ui */;
}

у меня есть несколько способов, которые я знаю/подозреваю/предполагаю, есть некоторые вопросы:

  1. Via reinterpret_cast,

    float reinterpret_as_float(std::uint32_t ui) {
        return reinterpret_cast<float&>(ui);
    }
    

    или

    float reinterpret_as_float(std::uint32_t ui) {
        return *reinterpret_cast<float*>(&ui);
    }
    

    который страдает от сглаживания проблемы.

  2. Via union,

    float reinterpret_as_float(std::uint32_t ui) {
        union {
            std::uint32_t ui;
            float f;
        } u = {ui};
        return u.f;
    }
    

    что на самом деле не является законным, так как разрешено читать только из самых последних написанных членам. Тем не менее, некоторые компиляторы (gcc) позволяют это.

  3. Via std::memcpy,

    float reinterpret_as_float(std::uint32_t ui) {
        float f;
        std::memcpy(&f, &ui, 4);
        return f;
    }
    

    который AFAIK является законным, но вызов функции для копирования одного слова кажется расточительным, хотя он может быть оптимизирован.

  4. Via reinterpret_castна char* и копируя,

    float reinterpret_as_float(std::uint32_t ui) {
        char* uip = reinterpret_cast<char*>(&ui);
        float f;
        char* fp = reinterpret_cast<char*>(&f);
        for (int i = 0; i < 4; ++i) {
            fp[i] = uip[i];
        }
        return f;
    }
    

    , который, насколько мне известно, тоже юридическое, как char указатели освобождаются от проблем с псевдонимами, а цикл ручного копирования байтов сохраняет возможный вызов функции. Цикл, безусловно, будет развернут, но 4, возможно, отдельные однобайтовые нагрузки/хранилища вызывают беспокойство, я понятия не имею, оптимизируется ли это для одной четырехбайтовой загрузки / хранилища.

на 4 лучшее, что я смог придумать.

я прав до сих пор? Есть ли лучший способ сделать это, в частности, тот, который гарантирует одиночную загрузку/хранение?

4 ответов


Afaik, есть только два подхода, которые соответствуют строгим правилам сглаживания:memcpy() и приведения к char* при копировании. Все остальные читают float из памяти, которая принадлежит uint32_t, и компилятору разрешено выполнять чтение перед записью в это место памяти. Он может даже оптимизировать запись вообще, поскольку он может доказать, что сохраненное значение никогда не будет использоваться в соответствии со строгими правилами псевдонима, что приводит к возвращению значения мусора.

это действительно зависит от компилятора/оптимизирует ли memcpy() или char* копировать быстрее. В обоих случаях интеллектуальный компилятор может понять, что он может просто загрузить и скопировать uint32_t, но я бы не доверял компилятору делать это, прежде чем я увижу его в результирующем коде ассемблера.

Edit:
После некоторого тестирования с gcc 4.8.1 я могу сказать, что memcpy() подход является лучшим для этого компилятора particulare, см. ниже подробности.


сборка

#include <stdint.h>

float foo(uint32_t a) {
    float b;
    char* aPointer = (char*)&a, *bPointer = (char*)&b;
    for( int i = sizeof(a); i--; ) bPointer[i] = aPointer[i];
    return b;
}

С gcc -S -std=gnu11 -O3 foo.c дает собрать этот код:

movl    %edi, %ecx
movl    %edi, %edx
movl    %edi, %eax
shrl    , %ecx
shrl    , %edx
shrw    , %ax
movb    %cl, -1(%rsp)
movb    %dl, -2(%rsp)
movb    %al, -3(%rsp)
movb    %dil, -4(%rsp)
movss   -4(%rsp), %xmm0
ret

это не оптимально.

делаешь то же самое с

#include <stdint.h>
#include <string.h>

float foo(uint32_t a) {
    float b;
    char* aPointer = (char*)&a, *bPointer = (char*)&b;
    memcpy(bPointer, aPointer, sizeof(a));
    return b;
}

выходы (со всеми уровнями оптимизации, кроме -O0):

movl    %edi, -4(%rsp)
movss   -4(%rsp), %xmm0
ret

этот вариант является оптимальным.


Если bitpattern в целочисленной переменной совпадает с допустимым float значение, тогда объединение, вероятно, лучший и наиболее совместимый способ пойти. И это на самом деле законно, если Вы читаете спецификацию (не помните раздел на данный момент).


memcpy всегда безопасен, но включает в себя копию

литье может привести к проблемам

union-кажется, разрешено в C99 и C11, не уверен в C++

взгляните на:

что такое строгое правило антиалиасинга?

и

является ли тип-каламбур через соединение неуказанным в C99, и он стал указан в C11?


float reinterpret_as_float(std::uint32_t ui) {
   return *((float *)&ui);
}

как простая функция, ее код переводится в сборку следующим образом (Pelles C для Windows):

fld [esp+4]
ret

если он определен как inline функция, затем такой код (n будучи неподписанными, x будучи поплавка):

x = reinterpret_as_float (n);

переводится на ассемблер следующим образом:

fld [ebp-4]  ;RHS of asignment. Read n as float
fstp dword ptr [ebp-8]  ;LHS of asignment