Boost C++ Serialization Overhead

Boost serialization performance: text vs. binary format

I used boost.serialization to store matrices and vectors representing lookup tables and
some meta data (strings) with an in memory size of about 200MByte. IIRC for loading from
disk into memory it took 3 minutes for the text archive vs. 4 seconds using the binary archive
on WinXP.

Why is there space overhead when deserializing from a binary archive into a std::map

Of course it makes sense.

E.g. when /tmp/all_params is a file generated with the following program:

Live On Coliru

#include <boost/serialization/map.hpp>
#include <boost/archive/binary_oarchive.hpp>
#include <boost/archive/binary_iarchive.hpp>
#include <boost/random.hpp>
#include <boost/bind.hpp>

struct myParam {
std::string data;
template <typename Ar> void serialize(Ar& ar, unsigned) {
ar & data;
}
};

static inline std::string generate_value() {
static auto rand_char = boost::bind(boost::uniform_int<unsigned char>(0,255), boost::mt19937{});

std::string s;
std::generate_n(back_inserter(s), rand_char(), rand_char);
return s;
}

using Map = std::map<unsigned int,myParam>;

Map generate_data(unsigned n) {
Map map;

for (unsigned i=0; i<n; ++i)
map.emplace(i, myParam { generate_value() });

return map;
}

#include <fstream>
#include <iostream>

int main() {
{
std::ofstream ofs("/tmp/all_params", std::ios::binary);
boost::archive::binary_oarchive oa(ofs);

auto data = generate_data(10ul<<19);
oa << data;
std::cout << "Serialized " << data.size() << " entries\n";
}
}

The file was 698miB on my system. The memory footprint looks like this (takes a while:)

==27420== Memcheck, a memory error detector
==27420== Copyright (C) 2002-2013, and GNU GPL'd, by Julian Seward et al.
==27420== Using Valgrind-3.10.0.SVN and LibVEX; rerun with -h for copyright info
==27420== Command: ./test
==27420==
Serialized 5242880 entries
==27420==
==27420== HEAP SUMMARY:
==27420== in use at exit: 0 bytes in 0 blocks
==27420== total heap usage: 47,021,247 allocs, 47,021,247 frees, 3,069,877,283 bytes allocated
==27420==
==27420== All heap blocks were freed -- no leaks are possible
==27420==

The peak usage snapshot was at 1.2 GiB:

Sample Image

Of course you can optimize the memory layout, e.g. by using Boost Flat Map (with the ordered_unique_range_t insertion overloads!) and a custom allocator for e.g. the strings there. This will reduce/eliminate the overhead:

Sample Image

The tweaked code:

#include <boost/serialization/map.hpp>
#include <boost/serialization/collections_load_imp.hpp>
#include <boost/serialization/collections_save_imp.hpp>
#include <boost/container/flat_map.hpp>
#include <boost/archive/binary_oarchive.hpp>
#include <boost/archive/binary_iarchive.hpp>
#include <boost/random.hpp>
#include <boost/bind.hpp>
#include <boost/utility/string_ref.hpp>
#include <cassert>

namespace string_pool {
static auto pool = []{
std::vector<char> init;
init.reserve(700ul<<20); // 700MiB
return init;
}();

using entry = boost::string_ref;

entry add(std::string const& s) {
assert((pool.capacity() >= (pool.size() + s.size())));

auto it = pool.end();
pool.insert(it, s.begin(), s.end());
return { &*it, s.size() };
}

static inline entry generate_random() {
static auto rand_char = boost::bind(boost::uniform_int<unsigned char>(0,255), boost::mt19937{});

static std::string s; // non-reentrant, but for lazy demo
s.resize(rand_char());
std::generate_n(s.begin(), s.size(), rand_char);
return add(s);
}
}

struct myParam {
string_pool::entry data;

template <typename Ar> void save(Ar& ar, unsigned) const {
std::string s = data.to_string();
ar & s;
}
template <typename Ar> void load(Ar& ar, unsigned) {
std::string s;
ar & s;
data = string_pool::add(s);
}
BOOST_SERIALIZATION_SPLIT_MEMBER()
};

// flat map serialization
namespace boost {
namespace serialization {

template<class Archive, typename...TArgs>
inline void save(
Archive & ar,
const boost::container::flat_map<TArgs...> &t,
const unsigned int /* file_version */
){
boost::serialization::stl::save_collection<
Archive,
boost::container::flat_map<TArgs...>
>(ar, t);
}

template<class Archive, typename...TArgs>
inline void load(Archive & ar, boost::container::flat_map<TArgs...> &t, const unsigned int /* file_version */) {
boost::serialization::stl::load_collection<Archive, boost::container::flat_map<TArgs...>,
boost::serialization::stl::archive_input_map<Archive, boost::container::flat_map<TArgs...> >,
boost::serialization::stl::reserve_imp <boost::container::flat_map<TArgs...> >
>(ar, t);
}

// split non-intrusive serialization function member into separate
// non intrusive save/load member functions
template<class Archive, typename...TArgs>
inline void serialize(Archive & ar, boost::container::flat_map<TArgs...> &t, const unsigned int file_version) {
boost::serialization::split_free(ar, t, file_version);
}
}
}

using Map = boost::container::flat_map<unsigned int,myParam>;

Map generate_data(unsigned n) {
Map map;
map.reserve(n);
std::cout << "Capacity: " << map.capacity() << "\n";

for (unsigned i=0; i<n; ++i)
map.emplace(i, myParam { string_pool::generate_random() });

std::cout << "Capacity: " << map.capacity() << "\n";
std::cout << "Total length: " << std::accumulate(
map.begin(), map.end(), 0ul, [](size_t acc, Map::value_type const& v) {
return acc + v.second.data.size();
}) << "\n";
return map;
}

#include <fstream>
#include <iostream>

int main() {
{
std::ofstream ofs("/tmp/all_params", std::ios::binary);
boost::archive::binary_oarchive oa(ofs);

auto data = generate_data(10ul<<19);
oa << data;
std::cout << "Serialized " << data.size() << " entries\n";
}
}

The md5sum of the generated /tmp/all_params file matched that of the first version: ac75521dc0dc65585368677c834613cb, proving that the data serialized is actually the same.

how to suppress the extra information in boost serialization::archive?

That's not a Dll version. It's the archive header.

Suppress it by using archive flags it:

void save_schedule(const bus_schedule &s, const char * filename){
// make an archive
std::ofstream ofs(filename);
boost::archive::text_oarchive oa(ofs, boost::archive::archive_flags::no_header);
oa << s;
}

And remember to do the same on restoring, of course!

void restore_schedule(bus_schedule &s, const char * filename) {
// open the archive
std::ifstream ifs(filename);
boost::archive::text_iarchive ia(ifs, boost::archive::archive_flags::no_header);

// restore the schedule from the archive
ia >> s;
}

See also

  • Boost binary archives - reducing size
  • Why does an non-intrusive serialization add a 5 byte zero prefix?
  • Boost C++ Serialization overhead

Is it safe to use boost serialization to serialize objects in C++ to a binary format for use over a socket?

No, in general boost binary serialization is not machine-independent. See here.

How to make boost::serialization deserialization faster?

If I had to pick the single biggest drawback of Boost.Serialization, it would be poor performance. If 400ms is truly too slow, either get faster hardware or switch to a different serialization library.

That said, just in case you're doing something blatantly "wrong", you should post the serialization code for Container, Container::SmallObject, and Container::SmallObject::CustomData. You should also ensure that it's actually deserialization that's taking 400ms, and not a combination of deserializing + reading the data from the disk; i.e., load the data into a memory-stream of some sort and deserialize from that, rather than deserializing from an std::fstream.


EDIT (in response to comments):

This code works for me using VC++ 2010 SP1 and Boost 1.47 beta:

double loadArchive(std::string const& archiveFileName, Container& data)
{
std::ifstream fileStream(
archiveFileName.c_str(),
std::ios_base::binary | std::ios_base::in
);
std::stringstream buf(
std::ios_base::binary | std::ios_base::in | std::ios_base::out
);
buf << fileStream.rdbuf();
fileStream.close();

StartCounter();
boost::archive::binary_iarchive(buf) >> data;
return GetCounter();
}

If this doesn't work for you, it must be specific to the compiler and/or version of Boost you're using (which are what?).

On my machine, for an x86 release build (with link-time code generation enabled), loading the data from disk is ~9% of the overall time taken to deserialize a 1.28MB file (1 Container containing 13000 SmallObject instances, each containing 4 CustomData instances); for an x64 release build, loading the data from disk is ~17% of the overall time taken to deserialize a 1.53MB file (same object counts).

Does Boost.Serialization serialize differently on different platforms?

try using a text_iarchive and text_oarchive instead of binary archives. From the documentation

In this tutorial, we have used a
particular archive class -
text_oarchive for saving and
text_iarchive for loading. text
archives render data as text and are
portable across platforms. In addition
to text archives, the library includes
archive class for native binary data
and xml formatted data. Interfaces to
all archive classes are all identical.
Once serialization has been defined
for a class, that class can be
serialized to any type of archive.

C++ serialization of complex data using Boost

There are many advantages to boost.serialization. For instance, as you say, just including a method with a specified signature, allows the framework to serialize and deserialize your data. Also, boost.serialization includes serializers and readers for all the standard STL containers, so you don't have to bother if all keys have been stored (they will) or how to detect the last entry in the map when deserializing (it will be detected automatically).

There are, however, some considerations to make. For example, if you have a field in your class that it is calculated, or used to speed-up, such as indexes or hash tables, you don't have to store these, but you have to take into account that you have to reconstruct these structures from the data read from the disk.

As for the "file format" you mention, I think some times we try to focus in the format rather than in the data. I mean, the exact format of the file don't matter as long as you are able to retrieve the data seamlessly using (say) boost.serialization. If you want to share the file with other utilities that don't use serialization, that's another thing. But just for the purposes of (de)serialization, you don't have to care about the internal file format.

Boost: Re-using/clearing text_iarchive for de-serializing data from Asio:receive()

No there is not such a way.

The comparison to MemoryStream is broken though, because the archive is a layer above the stream.

You can re-use the stream. So if you do the exact parallel of a MemoryStream, e.g. boost::iostreams::array_sink and/or boost::iostreams::array_source on a fixed buffer, you can easily reuse the buffer in you next (de)serialization.

See this proof of concept:

Live On Coliru

#include <boost/archive/text_iarchive.hpp>
#include <boost/archive/text_oarchive.hpp>
#include <boost/serialization/serialization.hpp>

#include <boost/iostreams/device/array.hpp>
#include <boost/iostreams/stream.hpp>
#include <sstream>

namespace bar = boost::archive;
namespace bio = boost::iostreams;

struct Packet {
int i;
template <typename Ar> void serialize(Ar& ar, unsigned) { ar & i; }
};

namespace Reader {
template <typename T>
Packet deserialize(T const* data, size_t size) {
static_assert(boost::is_pod<T>::value , "T must be POD");
static_assert(boost::is_integral<T>::value, "T must be integral");
static_assert(sizeof(T) == sizeof(char) , "T must be byte-sized");

bio::stream<bio::array_source> stream(bio::array_source(data, size));
bar::text_iarchive ia(stream);
Packet result;
ia >> result;

return result;
}

template <typename T, size_t N>
Packet deserialize(T (&arr)[N]) {
return deserialize(arr, N);
}

template <typename T>
Packet deserialize(std::vector<T> const& v) {
return deserialize(v.data(), v.size());
}

template <typename T, size_t N>
Packet deserialize(boost::array<T, N> const& a) {
return deserialize(a.data(), a.size());
}
}

template <typename MutableBuffer>
void serialize(Packet const& data, MutableBuffer& buf)
{
bio::stream<bio::array_sink> s(buf.data(), buf.size());
bar::text_oarchive ar(s);

ar << data;
}

int main() {
boost::array<char, 1024> arr;

for (int i = 0; i < 100; ++i) {
serialize(Packet { i }, arr);

Packet roundtrip = Reader::deserialize(arr);
assert(roundtrip.i == i);
}
std::cout << "Done\n";
}

For general optimization of boost serialization see:

  • how to do performance test using the boost library for a custom library
  • Boost C++ Serialization overhead
  • Boost Serialization Binary Archive giving incorrect output
  • Tune things (boost::archive::no_codecvt, boost::archive::no_header, disable tracking etc.)
  • Outputting more things than a Polymorphic Text Archive and Streams Are Not Archives


Related Topics



Leave a reply



Submit