Переносимость двоичной сериализации типа double/float в C++
Стандарт C++ не обсуждает базовое расположение типов с плавающей точкой и типа double, а только диапазон значений, которые они должны представлять. (Это также верно для подписанных типов, это комплимент двух или что-то еще)
Мой вопрос: какие методы используются для сериализации / десериализации типов POD, таких как double и float, переносимым способом? На данный момент кажется, что единственный способ сделать это состоит в том, чтобы значение было представлено буквально (как в "123.456"). Макет ieee754 для double не является стандартным для всех архитектур.
9 ответов
Брайан Холл "Beej Jorgensen" дает в своем Руководстве по сетевому программированию некоторый код float
(Соотв. double
) чтобы uint32_t
(Соотв. uint64_t
) иметь возможность безопасно передавать его по сети между двумя машинами, которые могут не согласиться с их представлением. У него есть некоторые ограничения, в основном он не поддерживает NaN и бесконечность.
Вот его функция упаковки:
#define pack754_32(f) (pack754((f), 32, 8))
#define pack754_64(f) (pack754((f), 64, 11))
uint64_t pack754(long double f, unsigned bits, unsigned expbits)
{
long double fnorm;
int shift;
long long sign, exp, significand;
unsigned significandbits = bits - expbits - 1; // -1 for sign bit
if (f == 0.0) return 0; // get this special case out of the way
// check sign and begin normalization
if (f < 0) { sign = 1; fnorm = -f; }
else { sign = 0; fnorm = f; }
// get the normalized form of f and track the exponent
shift = 0;
while(fnorm >= 2.0) { fnorm /= 2.0; shift++; }
while(fnorm < 1.0) { fnorm *= 2.0; shift--; }
fnorm = fnorm - 1.0;
// calculate the binary form (non-float) of the significand data
significand = fnorm * ((1LL<<significandbits) + 0.5f);
// get the biased exponent
exp = shift + ((1<<(expbits-1)) - 1); // shift + bias
// return the final answer
return (sign<<(bits-1)) | (exp<<(bits-expbits-1)) | significand;
}
Что не так с читаемым человеком форматом.
У него есть несколько преимуществ перед двоичным:
- Это читабельно
- Это портативный
- Это делает поддержку очень простой
(как вы можете попросить пользователя посмотреть на него в своем любимом редакторе даже слово) - Это легко исправить
(или настройте файлы вручную в случае ошибки)
Недостаток:
- Это не компактно
Если это реальная проблема, вы всегда можете ее застегнуть. - Это может быть немного медленнее извлекать / генерировать
Обратите внимание, что двоичный формат, вероятно, также должен быть нормализован (см.htonl()
)
Чтобы вывести двойной с полной точностью:
double v = 2.20;
std::cout << std::setprecision(std::numeric_limits<double>::digits) << v;
ХОРОШО. Я не уверен, что это точно. Это может потерять точность.
Взгляните на (старую) реализацию файла gtypes.h в glib 2 - она включает в себя следующее:
#if G_BYTE_ORDER == G_LITTLE_ENDIAN
union _GFloatIEEE754
{
gfloat v_float;
struct {
guint mantissa : 23;
guint biased_exponent : 8;
guint sign : 1;
} mpn;
};
union _GDoubleIEEE754
{
gdouble v_double;
struct {
guint mantissa_low : 32;
guint mantissa_high : 20;
guint biased_exponent : 11;
guint sign : 1;
} mpn;
};
#elif G_BYTE_ORDER == G_BIG_ENDIAN
union _GFloatIEEE754
{
gfloat v_float;
struct {
guint sign : 1;
guint biased_exponent : 8;
guint mantissa : 23;
} mpn;
};
union _GDoubleIEEE754
{
gdouble v_double;
struct {
guint sign : 1;
guint biased_exponent : 11;
guint mantissa_high : 20;
guint mantissa_low : 32;
} mpn;
};
#else /* !G_LITTLE_ENDIAN && !G_BIG_ENDIAN */
#error unknown ENDIAN type
#endif /* !G_LITTLE_ENDIAN && !G_BIG_ENDIAN */
Просто запишите двоичное представление IEEE754 на диск и запишите это как формат хранения (наряду с порядком байтов). Тогда это до реализации, чтобы преобразовать это к его внутреннему представлению при необходимости.
Создайте соответствующий интерфейс сериализатора / десериализатора для записи / чтения этого.
Интерфейс может иметь несколько реализаций, и вы можете проверить свои параметры.
Как уже было сказано, очевидными вариантами будут:
- IEEE754, который записывает / читает двоичный блок, если он напрямую поддерживается архитектурой, или анализирует его, если он не поддерживается архитектурой.
- Текст: всегда нужно разобрать.
- Все, что вы еще можете думать о.
Просто запомните - если у вас есть этот слой, вы всегда можете начать с IEEE754, если вы поддерживаете только платформы, которые используют этот формат внутри. Таким образом, вы будете прилагать дополнительные усилия только тогда, когда вам потребуется поддержка другой платформы! Не делай работу, которой ты не обязан.
Вы должны преобразовать их в формат, который вы всегда сможете использовать для воссоздания ваших чисел с плавающей запятой / двойников.
Это может использовать строковое представление или, если вам нужно что-то, что занимает меньше места, представить ваше число в ieee754 (или любом другом формате, который вы выберете), а затем проанализировать его, как если бы это было со строкой.
Нашел эту старую ветку. Отсутствует одно решение, которое решает большое количество случаев - использование фиксированной точки, передача целых чисел с известным коэффициентом масштабирования с использованием встроенных приведений в обоих концах. Таким образом, вам вообще не нужно беспокоиться о базовом представлении с плавающей точкой.
Есть конечно недостатки. Это решение предполагает, что вы можете иметь фиксированный коэффициент масштабирования и при этом получать диапазон и разрешение, необходимые для конкретного приложения. Кроме того, вы преобразуете из числа с плавающей точкой в фиксированную точку в конце сериализации и возвращаете обратно при десериализации, внося две ошибки округления. Однако на протяжении многих лет я обнаружил, что фиксированная точка достаточна для моих нужд почти во всех случаях, и она также достаточно быстра.
Типичным случаем для фиксированной точки являются протоколы связи для встроенных систем или других устройств.
SQLite4 использует новый формат для хранения значений типа double и float
- Он работает надежно и стабильно даже на платформах, в которых отсутствует поддержка двоичных чисел IEEE 754 с плавающей запятой.
- Вычисления валюты обычно могут быть выполнены точно и без округления.
- Любое 64-разрядное целое число со знаком или без знака может быть представлено точно.
- Диапазон и точность с плавающей запятой превышают таковые для IEEE 754 двоичных чисел с плавающей запятой64.
- Положительная и отрицательная бесконечность и NaN (Not-a-Number) имеют четко определенные представления.
Источники:
Я думаю, что ответ "зависит" от вашего конкретного приложения и его профиля производительности.
Допустим, у вас есть среда рыночных данных с низкой задержкой, тогда использование строк откровенно глупо. Если передаваемая вами информация - это цены, то с двойными (и их двоичным представлением) действительно сложно работать. Если, если вы действительно не заботитесь о производительности, а вам нужна видимость (хранение, передача), то строки являются идеальным кандидатом.
Я бы на самом деле выбрал целочисленное представление мантиссы / экспоненты чисел с плавающей запятой / двойников - то есть при первой же возможности преобразовал бы число с плавающей запятой / двойной в пару целых чисел и затем передал бы это. Тогда вам нужно только беспокоиться о переносимости целых чисел, а также о различных подпрограммах (таких как hton()
подпрограммы для обработки конверсий для вас). Кроме того, храните все в порядке, наиболее распространенном в вашей платформе (например, если вы используете только Linux, то какой смысл хранить вещи в формате с прямым порядком байтов?)