【问题标题】:Issues saving double as binary in c++在 C++ 中将 double 保存为二进制文件的问题
【发布时间】:2019-10-15 14:55:48
【问题描述】:

在我的粒子系统模拟代码中,我为粒子定义了一个类,每个粒子都有一个包含其位置的属性pos,即double pos[3];,因为每个粒子有3 个坐标分量。所以用particles = new Particle[npart];定义的粒子对象(因为我们有npart许多粒子),然后例如第二个粒子的 y 分量将通过double dummycomp = particles[1].pos[1]; 访问

为了在使用二进制文件之前将粒子保存到文件中,我会使用(保存为 txt,浮点精度为 10,每行一个粒子):

#include <iostream>
#include <fstream>

ofstream outfile("testConfig.txt", ios::out);
outfile.precision(10);

  for (int i=0; i<npart; i++){
    outfile << particle[i].pos[0] << " " << particle[i].pos[1]  << " " << particle[i].pos[2] << endl;
}
outfile.close();

但现在,为了节省空间,我尝试将配置保存为二进制文件,我的尝试受here 启发,如下所示:

ofstream outfile("test.bin", ios::binary | ios::out);

for (int i=0; i<npart; i++){ 
outfile.write(reinterpret_cast<const char*>(particle[i].pos),streamsize(3*sizeof(double))); 
}
outfile.close();

但我在尝试运行它时遇到了分段错误。我的问题是:

  • 我在 reinterpret_cast 或者更确切地说是在 streamsize() 的论点中做错了吗?
  • 理想情况下,如果保存的二进制格式也可以在 Python 中读取,那就太好了,我的方法(一旦修复)是否允许这样做?

旧保存方法的工作示例(非二进制):

#include <iostream>
#include <fstream>

using namespace std;
class Particle {

 public:

  double pos[3];

};


int main() {

  int npart = 2;
  Particle particles[npart];
  //initilizing the positions:
  particles[0].pos[0] = -74.04119568;
  particles[0].pos[1] = -44.33692582;
  particles[0].pos[2] = 17.36278231;

  particles[1].pos[0] = 48.16310086;
  particles[1].pos[1] = -65.02325252;
  particles[1].pos[2] = -37.2053818;

  ofstream outfile("testConfig.txt", ios::out);
  outfile.precision(10);

    for (int i=0; i<npart; i++){
      outfile << particles[i].pos[0] << " " << particles[i].pos[1]  << " " << particles[i].pos[2] << endl;
  }
  outfile.close();

    return 0;
}

并且为了将粒子位置保存为二进制,将上述样本的保存部分替换为

  ofstream outfile("test.bin", ios::binary | ios::out);

  for (int i=0; i<npart; i++){
  outfile.write(reinterpret_cast<const char*>(particles[i].pos),streamsize(3*sizeof(double))); 
  }
  outfile.close();

第二个附录:在 Python 中读取二进制文件

我设法使用 numpy 在 python 中读取保存的二进制文件,如下所示:

data = np.fromfile('test.bin', dtype=np.float64)
data
array([-74.04119568, -44.33692582,  17.36278231,  48.16310086,
       -65.02325252, -37.2053818 ])

但是考虑到 cmets 对二进制格式的不可移植性提出的质疑,我不相信这种在 Python 中的阅读方式会一直有效!如果有人能阐明这种方法的可靠性,那就太好了。

【问题讨论】:

  • 旁注:使用ifstreamofstream时不需要指定方向; “i”表示“in”,“o”表示“out”,您很少需要显式关闭它们中的任何一个 - 析构函数会处理这些。
  • 请不要重新发明轮子。使用像 cerealBoost serialization 这样的健全的库进行序列化。
  • pos的定义是什么?是[3]吗?
  • 那么粒子对象定义为particles = new Particle[npart]; -- 在哪里?评论和描述不是事实,只有代码才是事实。请发帖minimal reproducible example
  • 当您需要联系reinterpret_cast 时,您需要停下来想一想“我是不是在这里写了一个错误”,答案是通常“是”。跨度>

标签: c++ fstream binaryfiles


【解决方案1】:

问题在于 ascii 中 double 的以 10 为基数的表示存在缺陷,并且不能保证给您正确的结果(特别是如果您只使用 10 位数字)。即使您使用所有 std::numeric_limits&lt;max_digits10&gt; 数字,也可能会丢失信息,因为该数字可能无法以 10 为底数精确表示。

您遇到的另一个问题是 double 的二进制表示不标准化,因此使用它非常脆弱并且很容易导致代码破坏。简单地改变编译器或编译器位置可能会导致不同的双重格式和改变架构,你绝对不能保证。

您可以使用双精度的十六进制格式将其序列化为无损表示的文本。

 stream << std::fixed << std::scientific << particles[i].pos[0];

 // If you are using C++11 this was simplified to

 stream << std::hexfloat << particles[i].pos[0];

这具有打印与 C 中 printf() 中的“%a”相同的值的效果,将字符串打印为“十六进制浮点,小写”。这里radixmantissa 在以非常特定的格式打印之前都转换为十六进制值。由于底层表示是二进制的,因此这些值可以精确地以十六进制表示,并提供一种在系统之间传输数据的无损方式。 IT 还会截断后续的零,因此对于很多数字来说是相对紧凑的。

在 python 方面。也支持这种格式。您应该能够将值作为字符串读取,然后使用 float.fromhex() 将其转换为浮点数

见:https://docs.python.org/3/library/stdtypes.html#float.fromhex

但您的目标是节省空间:

但现在,为了节省空间,我尝试将配置保存为二进制文件。

我会问你真的需要节省空间吗?您是否在低功耗低资源环境中运行?当然,节省空间绝对是一件事(但现在很少见(但这些环境确实存在))。

但您似乎正在运行某种形式的粒子模拟。这并没有尖叫低资源用例。即使你有 tera 字节的数据,我仍然会使用一种可移植的易于阅读的二进制格式。最好是没有损耗的。存储空间便宜。

【讨论】:

  • 非常感谢所有的解释,这真的很有帮助!使用 hexfloat 真的很有趣!我现在要试一试!
  • 嗨马丁,我无法使用 hexfloat 写入文件,这有什么技巧吗? (即在上述代码 sn-p 中的outfile.write(...) 内。感谢您对此的任何提示。
  • @user929304 outfile &lt;&lt; std::hexfloat &lt;&lt; particles[i].pos[0] &lt; " ";
【解决方案2】:

我建议使用 而不是从头开始编写序列化/反序列化例程。我发现cereal 真的很容易使用,甚至可能比boost::serialization 更容易。它减少了您自己的代码中出现错误的机会。

在你的情况下,我会像这样使用谷物序列化doubles:

#include <cereal/archives/binary.hpp>
#include <fstream>

int main() {
    std::ofstream outfile("test.bin", ios::binary);
    cereal::BinaryOutputArchive out(outfile);
    double x, y, z;
    x = y = z = 42.0;
    out(x, y, z);
}

要反序列化它们,您可以使用:

#include <cereal/archives/binary.hpp>
#include <fstream>

int main() {
    std::ifstream infile("test.bin", ios::binary);
    cereal::BinaryInputArchive in(infile);
    double x,y,z;
    in(x, y, z);
}

您还可以以相同的方式序列化/反序列化整个std::vector&lt;double&gt;s。只需添加#include &lt;cereal/types/vector.hpp&gt; 并使用in / out 就像给定示例中的单个std::vector&lt;double&gt; 而不是多个doubles。

是不是很膨胀。


编辑

在您询问的评论中,是否可以使用 Python 读取创建的二进制文件。

答案:

序列化的二进制文件并不是真正的可移植性(字节序之类的东西可能在这里发挥作用)。您可以轻松修改我给您的示例代码以编写JSON 文件(使用库的另一个优点)和read that format in Python

哦,cereal::JSONOutputArchive 有一个 option for setting precision

【讨论】:

  • 您可能需要 truncout 标志用于 outfilein 用于 infile
  • 记住:std::to_chars / std::from_chars。不需要额外的库(除了 C++ 标准库)。
  • @MaximEgorushkin ifstream 默认为 inoutofstream
  • @JesperJuhl 来自OP's previous question 我推断最好推荐一个“完整”的问题解决方案。哦,麦片只是标题,所以那里的工作量很小(包括在内)。
  • @drescherjm 你没有错,你是对的。 XML 或 JSON 会更大但可移植。二进制会更小,但不可移植。折衷方案是portable binary 或压缩的 XML/JSON。
【解决方案3】:

只是好奇您是否曾经研究过将数据转换为矢量坐标而不是笛卡尔 X、Y、Z 坐标的想法?看起来这可能会将您的数据大小减少大约 30%:两个坐标而不是三个坐标,但可能需要稍微更高的精度才能转换回您的 X、Y、Z。

矢量坐标仍然可以通过使用上述各种压缩技术(文本压缩或二进制转换)进一步优化。

【讨论】:

    猜你喜欢
    • 2019-11-20
    • 1970-01-01
    • 2010-10-20
    • 1970-01-01
    • 2010-09-19
    • 2014-01-16
    • 1970-01-01
    • 2011-10-20
    • 1970-01-01
    相关资源
    最近更新 更多