Самый эффективный стандартизованный способ переинтерпретации 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
- лучшее, что я смог придумать.
До сих пор я исправляю? Есть ли лучший способ сделать это, особенно тот, который гарантирует единую загрузку/хранение?
Ответы
Ответ 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
, то объединение, вероятно, является лучшим и наиболее подходящим способом. И это действительно законно, если вы читаете спецификацию (не помните раздел в данный момент).
Ответ 3
memcpy всегда безопасен, но включает в себя копию
литье может привести к проблемам
union - похоже, разрешено в C99 и C11, не уверен в С++
Взгляните на:
Что такое строгое правило псевдонимов?
и
Является ли type-punning через объединение, не заданное в C99, и стало ли оно указано в C11?
Ответ 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