Проблемы сохранения двойного как бинарного в C ++ - PullRequest
1 голос
/ 15 октября 2019

В моем коде моделирования для системы частиц у меня есть класс, определенный для частиц, и каждая частица имеет свойство pos, содержащее ее положение, которое равно double pos[3];, поскольку на каждую частицу приходится 3 координатных компонента. Таким образом, если объект частицы определен как particles = new Particle[npart]; (так как у нас npart много частиц), то, например, y-компонент 2-й частицы будет доступен с помощью 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();

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

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();

2-е добавление: чтение двоичного кода в Python

Мне удалось прочитать сохраненный двоичный файл в python следующим образом, используя numpy:

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

Но, учитывая сомнения в комментариях относительно непереносимости двоичного формата, яне уверен, что этот тип чтения в Python всегда будет работать! Было бы очень хорошо, если бы кто-то мог объяснить надежность такого подхода.

Ответы [ 3 ]

2 голосов
/ 15 октября 2019

Проблема в том, что базовое 10 представление double в ascii некорректно и не гарантирует правильного результата (особенно если вы используете только 10 цифр). Существует вероятность потери информации, даже если вы используете все std::numeric_limits<max_digits10> цифры, так как число может не быть точно представлено в базе 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];

Это влияет на печать значения с тем же значением, что и «% a» в printf() в C, что печатает строку как «Шестнадцатеричная плавающая точка, строчные буквы». Здесь и radix, и mantissa преобразуются в шестнадцатеричные значения перед печатью в очень специфическом формате. Поскольку базовое представление является двоичным, эти значения могут быть представлены точно в шестнадцатеричном формате и обеспечивают способ передачи данных без потерь между системами. Он также усекает исходящие и последующие нули, поэтому для многих чисел он относительно компактен.

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

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

Но ваша цель - сэкономить место:

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

Я хотел бы задать вопрос: действительно ли вам нужно экономить место? Вы работаете в среде с низким энергопотреблением? Конечно, тогда экономия места может быть чем-то особенным (но в настоящее время это случается редко (но эти среды существуют)).

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

1 голос
/ 15 октября 2019

Я предлагаю использовать библиотеку вместо написания подпрограммы сериализации / десериализации с нуля. Я нахожу хлопья действительно простыми в использовании, может быть, даже проще, чем boost :: serialization . Это уменьшает вероятность ошибок в вашем собственном коде.

В вашем случае я бы пошел на сериализацию double s, например, используя cereal:

#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<double> s таким же образом. Просто добавьте #include <cereal/types/vector.hpp> и используйте in / out, как в данном примере, на одном std::vector<double> вместо нескольких double с.

Разве это не так?


Редактировать

В комментарии вы спросили, , можно ли будет прочитать созданный двоичный файл, подобный этому, с помощью Python.

Ответ:

Сериализованные двоичные файлы на самом деле не предназначены для того, чтобы быть очень переносимыми (такие вещи, как endianness могут играть здесь роль). Вы можете легко адаптировать пример кода, который я вам дал, для записи файла JSON (еще одно преимущество использования библиотеки) и чтения этого формата в Python .

Oh иcereal::JSONOutputArchive имеет опцию для установки точности .

0 голосов
/ 27 октября 2019

Просто любопытно, изучали ли вы когда-нибудь идею преобразования ваших данных в векторные координаты вместо декартовых X, Y, Z? Казалось бы, это потенциально уменьшит размер ваших данных примерно на 30%: две координаты вместо трех, но, возможно, потребуется немного более высокая точность для преобразования обратно в ваши X, Y, Z.

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

...