Google 的 Protocol Buffer 在实践中处理浮点类型的跨平台程度如何?

Posted

技术标签:

【中文标题】Google 的 Protocol Buffer 在实践中处理浮点类型的跨平台程度如何?【英文标题】:How cross-platform is Google's Protocol Buffer's handling of floating-point types in practice? 【发布时间】:2011-08-30 19:49:39 【问题描述】:

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 上被打破。如果我正确理解this page by Oracle describing the format used for number on SPARC,SPARC 使用与 x86 相反的字节序来表示整数但与 x86 相同的字节序用于浮点数和双精度数。但是,PB 对浮点数/双精度数进行编码,首先将它们直接转换为适当大小的整数类型(通过联合;请参阅上面我的问题中引用的代码的 sn-ps),然后反转字节的顺序具有大端整数的平台:

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 上的浮点数/双精度数的情况下,这样做是完全错误的,因为字节已经处于“正确”的顺序。

总之,如果我的理解是正确的,那么浮点数不能使用 PB 在 SPARC 和 x86 之间传输,因为本质上 PB 假定所有数字都以相同的字节序存储(相对于其他平台)作为给定平台上的整数,这是在 SPARC 上做出的错误假设。

更新 2

正如 Lyke 所指出的,与 x86 相比,IEEE 64 位浮点 在 SPARC 上以大端顺序存储。但是,只有两个 32 位字的顺序相反,而不是全部 8 个字节,特别是 IEEE 32 位浮点数看起来它们的存储顺序与 x86 上的顺序相同。

【问题讨论】:

P.S.:感谢 Lyke 提供 Oracle 页面的链接! :-) 那个 oracle 页面在 SPARC 与 x86 上的字节序表示方面看起来确实有点模棱两可,我怀疑 Jon Skeet 是对的。不过,鉴于上面的代码,如果您在同一主机上读取/写入它们会很好。我想说我们需要一个使用 SPARC 机器的人来生成带有双精度/浮点数的输出,然后用上面的代码在 x86 上读回它们来解决这个问题:) 我认为使用这样的联合会导致未定义的行为。 @Mike:理论上是的,但实际上它似乎可以用于此类目的,即使不能保证。从某种意义上说,您的评论重申了我问题的本质。 :-) @ nos:“我想说我们需要一个使用 SPARC 机器的人来生成带有双精度/浮点数的输出,然后用上面的代码在 x86 上读回它们来解决这个问题 :)”我不没有 SPARC 机器,但是(经过大量的血汗和泪水 :-))我设法获得了一个运行 NetBSD 的 qemu 仿真 SPARC 系统,并且果然在 SPARCH 上生成的输出得到了正确的结果在 x86 上回读时。 【参考方案1】:

我认为只要您的目标 C++ 平台使用 IEEE-754 并且库正确处理字节序,就应该没问题。基本上,您显示的代码假设如果您以正确的顺序获得正确的位和 IEEE-754 实现,您将获得正确的值。字节序由协议缓冲区处理,并且假定为 IEEE-754-ness - 但非常普遍。

【讨论】:

我可能读错了,但在 download.oracle.com/docs/cd/E18659_01/html/821-1384/bjbds.html(F.2.4 中的表格)上,在 x86 和 sparc 上表示双精度似乎有点不同。 该表显示了长双打的差异。 SPARC 上的 Long Doubles 是 128 位浮点数,而 Intel 使用 80 位浮点数。 实际上,在我看来,在该页面上,浮点数和双精度数是唯一在 x86 和 SPARC 上存储完全相同的数据类型。 @Gregory Crosswhite 很好,表格中提到了 x86 上的 +3.0、0000000040080000 和 sparc 上的 4008000000000000 @Gregory:在协议缓冲区中有带有“黄金”二进制文件和文本文件的单元测试。所以至少在这种情况下单元测试是这样工作的。【参考方案2】:

实际上,他们在执行字节序的情况下进行读写的事实足以保持可移植性。考虑到协议缓冲区在许多平台(甚至是语言)上的广泛使用,这一点相当明显。

【讨论】:

以上是关于Google 的 Protocol Buffer 在实践中处理浮点类型的跨平台程度如何?的主要内容,如果未能解决你的问题,请参考以下文章

Google Protocol Buffer

为 Google Protocol Buffer 解析文本文件

Google Protocol Buffer入门

Google Protocol Buffer 的使用和原理

Google Protocol Buffer 的使用和原理

Google Protocol Buffer 的使用和原理