Подтвердить что ты не робот

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

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

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

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

  • Через 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);
    }
    

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

  • Через union,

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

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

  • Через std::memcpy,

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

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

  • Через 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;
    }
    

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

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

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

4b9b3361

Ответ 1

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

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

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


компилирование

#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    $24, %ecx
shrl    $16, %edx
shrw    $8, %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

Это оптимально.

Ответ 2

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

Ответ 4

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