Насколько кроссплатформенным является на практике обработка типов с плавающей запятой в протоколе Google Protocol Buffer?

Буферы протокола Google позволяют хранить в сообщениях числа с плавающей или двойной точностью. Я просмотрел исходный код реализации, задаваясь вопросом, как им удалось сделать это кроссплатформенным способом, и наткнулся на следующее:

inline uint32 WireFormatLite::EncodeFloat(float value) {
  union {float f; uint32 i;};
  f = value;
  return i;
}

inline float WireFormatLite::DecodeFloat(uint32 value) {
  union {float f; uint32 i;};
  i = value;
  return f;
}

inline uint64 WireFormatLite::EncodeDouble(double value) {
  union {double f; uint64 i;};
  f = value;
  return i;
}

inline double WireFormatLite::DecodeDouble(uint64 value) {
  union {double f; uint64 i;};
  i = value;
  return f;
}

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

inline void WireFormatLite::WriteFloatNoTag(float value,
                                        io::CodedOutputStream* output) {
  output->WriteLittleEndian32(EncodeFloat(value));
}

inline void WireFormatLite::WriteDoubleNoTag(double value,
                                         io::CodedOutputStream* output) {
  output->WriteLittleEndian64(EncodeDouble(value));
}

template <>
inline bool WireFormatLite::ReadPrimitive<float, WireFormatLite::TYPE_FLOAT>(
    io::CodedInputStream* input,
    float* value) {
  uint32 temp;
  if (!input->ReadLittleEndian32(&temp)) return false;
  *value = DecodeFloat(temp);
  return true;
}

template <>
inline bool WireFormatLite::ReadPrimitive<double, WireFormatLite::TYPE_DOUBLE>(
    io::CodedInputStream* input,
    double* value) {
  uint64 temp;
  if (!input->ReadLittleEndian64(&temp)) return false;
  *value = DecodeDouble(temp);
  return true;
}

Итак, мой вопрос: действительно ли это достаточно хорошо на практике, чтобы гарантировать, что сериализация чисел с плавающей запятой и двойных чисел в C ++ будет переносима между платформами?

Я явно вставляю слова «на практике» в свой вопрос, потому что я знаю, что теоретически нельзя делать никаких предположений о том, как на самом деле форматируются числа с плавающей запятой и двойные числа в C ++, но у меня нет чувство того, действительно ли эта теоретическая опасность является чем-то, о чем я должен очень беспокоиться на практике.

ОБНОВЛЕНИЕ

Мне теперь кажется, что подход PB может быть нарушен в SPARC. Если я понимаю эту страницу Oracle, описывающую формат, используемый для чисел на SPARC правильно, SPARC использует обратный порядок байтов, как x86 для целых чисел , но тот же порядок байтов, что и x86 для чисел с плавающей запятой и удвоения. Однако PB кодирует числа с плавающей запятой / удвоения, сначала приводя их непосредственно к целочисленному типу соответствующего размера (с помощью объединения; см. Фрагменты кода, указанные в моем вопросе выше), а затем меняя порядок байтов на противоположный на платформах с целые числа с прямым порядком байтов:

void CodedOutputStream::WriteLittleEndian64(uint64 value) {
  uint8 bytes[sizeof(value)];

  bool use_fast = buffer_size_ >= sizeof(value);
  uint8* ptr = use_fast ? buffer_ : bytes;

  WriteLittleEndian64ToArray(value, ptr);

  if (use_fast) {
    Advance(sizeof(value));
  } else {
    WriteRaw(bytes, sizeof(value));
  }
}

inline uint8* CodedOutputStream::WriteLittleEndian64ToArray(uint64 value,
                                                            uint8* target) {
#if defined(PROTOBUF_LITTLE_ENDIAN)
  memcpy(target, &value, sizeof(value));
#else
  uint32 part0 = static_cast<uint32>(value);
  uint32 part1 = static_cast<uint32>(value >> 32);

  target[0] = static_cast<uint8>(part0);
  target[1] = static_cast<uint8>(part0 >>  8);
  target[2] = static_cast<uint8>(part0 >> 16);
  target[3] = static_cast<uint8>(part0 >> 24);
  target[4] = static_cast<uint8>(part1);
  target[5] = static_cast<uint8>(part1 >>  8);
  target[6] = static_cast<uint8>(part1 >> 16);
  target[7] = static_cast<uint8>(part1 >> 24);
#endif
  return target + sizeof(value);
}

Однако это как раз то, что нужно делать в случае с плавающей запятой / удвоением на SPARC, поскольку байты уже находятся в «правильном» порядке.

Итак, в заключение, если я правильно понимаю, тогда числа с плавающей запятой не переносятся между SPARC и x86 с использованием PB, потому что, по сути, PB предполагает, что все числа хранятся с тем же порядком байтов (относительно других платформ), что и целые числа на данной платформе, что является неправильным предположением для SPARC.

ОБНОВЛЕНИЕ 2

Как указал Лайк, 64-битные числа с плавающей запятой IEEE хранятся в SPARC в обратном порядке, в отличие от x86. Однако только два 32-битных слова находятся в обратном порядке, а не все 8 байтов, и в частности 32-битные числа с плавающей запятой IEEE выглядят так, как будто они хранятся в том же порядке, что и на x86.


person Gregory Crosswhite    schedule 30.08.2011    source источник
comment
P.S .: Спасибо Lyke за ссылку на страницу Oracle! :-)   -  person Gregory Crosswhite    schedule 31.08.2011
comment
Эта страница оракула выглядит немного двусмысленно относительно представления порядка байтов в SPARC и x86, и я подозреваю, что Джон Скит прав. Хотя, учитывая приведенный выше код, было бы неплохо, если бы вы читали / записываете их на одном и том же хосте. Я бы сказал, что нам нужен soneone с машиной SPARC, чтобы генерировать вывод с двойными / float и читать их обратно на x86 с помощью приведенного выше кода, чтобы решить эту проблему :)   -  person nos    schedule 31.08.2011
comment
Я думал, что использование таких союзов приводит к неопределенному поведению.   -  person    schedule 31.08.2011
comment
@ Mike: Теоретически да, но на практике кажется, что это работает для таких целей, хотя это и не гарантируется. В каком-то смысле ваш комментарий повторил суть моего вопроса. :-)   -  person Gregory Crosswhite    schedule 31.08.2011
comment
@ nos: я бы сказал, что нам нужен soneone с машиной SPARC для генерации вывода с double / float и считывания их обратно на x86 с помощью приведенного выше кода, чтобы решить эту проблему :) У меня нет машины SPARC, но (после удивительное количество крови, пота и слез :-)) Мне удалось получить систему SPARC с эмуляцией qemu, работающую с NetBSD, и, конечно же, сгенерированный вывод на SPARCH получил правильный результат при повторном чтении на x86.   -  person Gregory Crosswhite    schedule 01.09.2011


Ответы (2)


Я думаю, все будет в порядке, если ваша целевая платформа C ++ использует IEEE-754, а библиотека правильно обрабатывает порядок байтов. В основном код, который вы показали, предполагает, что если у вас есть правильные биты в правильном порядке и реализация IEEE-754, вы получите правильное значение. Порядок байтов обрабатывается буферами протокола, и предполагается соответствие стандарту IEEE-754, но довольно универсально.

person Jon Skeet    schedule 30.08.2011
comment
Скорее всего, я неправильно его читаю, но на download.oracle. com / docs / cd / E18659_01 / html / 821-1384 / bjbds.html (таблица в F.2.4) кажется, что есть небольшая разница в представлении двойников на x86 и sparc. - person Lyke; 31.08.2011
comment
В таблице указаны различия в длинных дублях. Длинные двойники в SPARC - это 128-битные числа с плавающей запятой, в то время как Intel использует 80-битные числа с плавающей запятой. - person Dave S; 31.08.2011
comment
На самом деле мне на этой странице кажется, что числа с плавающей запятой и удвоения - единственные типы данных, которые хранятся одинаково на x86 и SPARC. - person Gregory Crosswhite; 31.08.2011
comment
@Gregory Crosswhite ну, в таблице упоминается для +3.0, 0000000040080000 на x86 и 4008000000000000 на sparc - person Lyke; 31.08.2011
comment
@Lyke: Это разница в порядке байтов, а не в размере. Я ожидал, что разница в порядке байтов будет обрабатываться библиотекой compiler + protobuf. - person Jon Skeet; 31.08.2011
comment
Я согласен с Грегори здесь. SPARC следует спецификации IEEE754, как и Intel, поэтому у них не должно возникнуть проблем. Я почему-то сомневаюсь, что какое-либо оборудование не соответствует IEEE-754, по крайней мере, в той степени, в которой оно использует те же представления бит (я бы предположил, что оборудование, не соответствующее спецификации, вероятно, будет более снисходительным с точки зрения обработки денормальных значений или режимов округления и т. д. то есть вещи, которые сложно реализовать и в большинстве случаев не так уж и важны). - person Voo; 31.08.2011
comment
На самом деле, по иронии судьбы, мне кажется, что это может означать, что буферы протокола не работают на SPARC, поскольку на SPARC числа с плавающей запятой такие же, как x86, но PB все равно изменит порядок байтов на обратный, потому что SPARC - это платформа с прямым порядком байтов, и PB предполагает, что если целые числа имеют обратный порядок байтов и их байты должны быть перевернуты, то то же самое верно для чисел с плавающей запятой и удвоений. - person Gregory Crosswhite; 31.08.2011
comment
@Voo: Я подозреваю, что есть еще какое-то оборудование, которое не соответствует 754, но оно достаточно редкое, чтобы его можно было игнорировать :) - person Jon Skeet; 31.08.2011
comment
@Gregory: Я не знаю - я подозреваю, что целочисленный макет автоматически работает правильно. Одна из приятных особенностей IEEE-754 заключается в том, что его можно сортировать, если вы рассматриваете значения как целые числа (IIRC), и я сомневаюсь, что они пожертвуют этим. Я подозреваю, что это просто случай, когда целые числа имеют нечетное представление, если вы посмотрите на точный байтовый образец - но если вы рассматриваете его как два слова с прямым порядком байтов, это нормально. Помимо всего прочего, модульные тесты IIRC выполняют тестирование с плавающей запятой, поэтому они довольно быстро взорвутся, если это не удастся :) - person Jon Skeet; 31.08.2011
comment
@Jon Skeet Достаточно верно;) Я должен был сказать об оборудовании, которое вы увидите в дикой природе и которое может запускать protobuf, в конце концов, я уверен, что в музеях еще достаточно PDP-10 ;-) - person Voo; 31.08.2011
comment
@ Лайк, ты прав! Спасибо, что указали на это. Что интересно, меняются местами только слова, а не байты. - person Gregory Crosswhite; 31.08.2011
comment
ARM имеет аппаратные средства ABI и float, которые хранят числа с прямым порядком байтов в обратном порядке, в системах FPA с прямым порядком байтов IIRC. Возьми это, изверг. Как говорит Джон, конечно, модульный тест быстро это поймает. Если тестовые значения не выбраны на удивление плохо, порядок байтов не будет работать, когда он не работает. Судя по виду кода, цитируемого в вопросе, я сомневаюсь, что так сложно разветвить протоколы буферов для забавных архитектур и просто заменить WriteDoubleNoTag и т. Д., Если формат хранения близок к IEEE. - person Steve Jessop; 31.08.2011
comment
@ Steve: Как сказал Джон, конечно, модульный тест быстро поймет это. Да, но проблема в том, что написание модульного теста, в котором что-то создается на архитектуре, а затем читается на другом, - это боль ... тем не менее, если у меня будет время, я думаю, что использую это как предлог для игры с запускать виртуальную машину SPARC, чтобы лично убедиться, работает ли их код. Хотя, честно говоря, я начинаю приходить к выводу, что лучше всего просто записывать значения с плавающей запятой, которые я хочу сохранить в виде текста, а не использовать двоичный формат, поскольку выглядит так легко ошибиться. :-) - person Gregory Crosswhite; 31.08.2011
comment
@Gregory: В буферах протокола есть модульные тесты с золотыми двоичными файлами, а также текстовые файлы. Так, по крайней мере, в этом случае работают юнит-тесты. - person Jon Skeet; 31.08.2011

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

person Reed Copsey    schedule 30.08.2011