Performance Boost Serialization

Performance Boost serialization

Die letzte Woche hab ich mich ausführlich mir der Serializierung von Daten mit Boost beschäftigt. Es ging mir darum, wie man das ganze in einem zeitkritischem Pfad in einer Anwendung einzusetzen.

TLDR

Bandwidth

xml text binary
skalieren skalieren skalieren

Items/sec

xml text binary
skalieren skalieren skalieren

Eckdaten:

  • Debian Linux Stretch
  • gcc 6.1.1
  • AMD8350 @4GHz
  • 16GB Ram

Hier ist die Y-Achse logarithmisch. Beim Binary Bild denke ich das hier die Cache der CPU zuschlägt und das zugrundeliegende std::memcpy voll durchschlägt.

Details

Das Ziel der Bemühungen ist es ein System zu haben das:

  • C++ Typen zustellt. Meine Statemachines arbeiten mit boost::msm. Hier sollen die Typen ohne großen Overhead zum Ziel kommen.
  • Die Packete in nicht serialiserter Form in einer Queue ablegen kann.
  • Schnell ist
  • Im selben Prozess nicht serialiert
  • auf der selben Maschine über posix mq_send/mq_receive Daten zustellt
  • Über Sockets auch auf Remote Maschinen Daten zustellen kann.

Queue

Um die lokale Queue zu realisieren sind die Typen nicht von einer Basisklasse abgeleitet sondern in einen Carrier eingebunden.

class carrier_visitor_base;

class carrier_base // the base in the queue
{
public:
	using ptr = std::unique_ptr<carrier_base>;
	virtual ~carrier_base() {}
	virtual void accept(carrier_visitor_base *p_visitor) = 0;
};


template <typename T>
class carrier : public carrier_base // the specific carrier
{
public:
	explicit carrier() : m_data() {}
	explicit carrier(const T &data) : m_data(data) {}
	virtual void accept(carrier_visitor_base *p_visitor) override
	{
		p_visitor->handle(m_data);
	}
	T &data() { return m_data; }
private:
	T m_data;
};

Hier sieht man auch schon wie die Daten dann zugestellt werden. Ein Visitorpattern. Der Doubledispatch ist ziemlich günstig. Nur zwei Functioncalls.

Das Ziel der Packete ist dann schon ein Visitor:

class protocol : public carrier_visitor_base
{
public:
....
	virtual void handle(const ev_test &item) override;
..
};

Dieses Protocol, hat als pimpl die MSM Statemachine. Die handle Funktionen werfen die Packete dann per process_event() bzw queue_event() in die Statemachine.

Serialisierung

Jetzt sind die Packete Lokal zugestellt.

Um die Packete über eine Leitung, message_queue oder pipe zuzustellen müssen diese serialisiert werden. Da diese Serialisierung hier im Nachrichtenpfad liegt, kommt es natürlich auf die Performance an.

Boost serialization nutze ich viel, nur bisher nicht in einem zeitkritischen Pfad. Die Messungen die ich bisher gefunden habe, haben meist nur einen Archiv Typ gezeigt. Binary.

Was ich hier an diesen Daten schön finde, dass man hier erkennen kann, das die Bandbreite an einem Limit anschlägt. Je kleiner die Daten sind, umso mehr macht der Overhead des Archives aus. Insgesamt, ist das natürlich immer noch sehr wenig im Vergleich zu großen Daten.

Sehr großen Einfluss hat auch noch wie die Daten ins Archiv geschoben werden.

	boost::serialization::make_binary_object(t.m_data.data(), t.m_data.size()));

vs

	boost::serialization::make_array(t.m_data.data(), t.m_data.size()));

make_binary_object hat hier eine Bandbreite von ca 50 MB/s und XML Archiv, make_array hat nur 2.5 Mb/s. make_array ist hier bei den textuellen Archiven um Faktor 20 langsamer. Beim binary Archiv, ist hier der Faktor nicht so groß. Die Spite ist beides mal 7.9 GB/s. Bei mehreren Samples liegt es aber etwas under dem binary_object.

Einen Nachteil hat das binary object:

// serialization function for ev_test
template <class Archive>
inline void save(Archive &ar, const ev_test &t, const unsigned int version)
{
	size_t size = t.m_data.size();
	ar &BOOST_SERIALIZATION_NVP(size);
	ar &boost::serialization::make_nvp(
		"m_data",
		boost::serialization::make_binary_object(const_cast<uint8_t*>(t.m_data.data()), t.m_data.size()));
}

Der const_cast<>….

== std::locale == Sehr großen Einfluss auf die Perfommance hatte auch das globale setzen des locales bei den textuellen Archiven. Hier ist die benötigte Zeit pro Item bei kleinen Items auf die Hälfte geschrumpft. (80µs -> 40 µs). Hier wird dann nicht pro Item ein Locale gesetzt und dann wieder zurückgenommen. Je größer das Item ist, umso weniger Einfluss hat das.

std::locale::global(std::locale("C"));

Windows

Ich hab das ganze auch auf Windows ausprobiert. Insgesamt zeigt sich hier ein ähnliches Bild. Nur der MSVC 2015 Community Edition war hier deutlich langsamer. Mit msys2 und gcc 5.3.0 war ich dann fast auf Linux Niveau. Hier hat auf der boost-users Mailing List auch keiner eine Antwort gewusst.

Hier der Vergleich Linux / Windows (MSVC)

Linux Windows (MSVC)
skalieren skalieren
skalieren skalieren
skalieren skalieren
skalieren skalieren
skalieren skalieren
skalieren skalieren

Der Code

// STL Archive + Stuff
#include <boost/serialization/base_object.hpp>
#include <boost/serialization/binary_object.hpp>
#include <boost/serialization/export.hpp>
#include <boost/serialization/shared_ptr.hpp>
#include <boost/serialization/split_free.hpp>
#include <boost/serialization/unique_ptr.hpp>

// include headers that implement a archives in xml/text/binary format
#include <boost/archive/archive_exception.hpp>

#include <boost/archive/xml_iarchive.hpp>
#include <boost/archive/xml_oarchive.hpp>

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

#include <boost/archive/binary_iarchive.hpp>
#include <boost/archive/binary_oarchive.hpp>

// IO stream for the to/from wire functions
#include <boost/iostreams/device/array.hpp>
#include <boost/iostreams/device/back_inserter.hpp>
#include <boost/iostreams/stream.hpp>

#include <memory>
#include <cstdint>
#include <vector>

#include <benchmark/benchmark.h>

// the step interval for the benchmarks
static const int range_mult = 4;
static const int range_max_step = 20;

// the test structure
struct ev_test
{
	ev_test(size_t s = 0)
	{
		m_data.resize(s);
		for (auto &c : m_data)
			c = 1;
	}

	std::vector<uint8_t> m_data;
};

//-----------------------------------------------------------------------------
// Type carrier and its support
//----------------------------------------------------------------------------
namespace net
{
using packet = std::vector<char>; // a packet on the wire

class carrier_visitor_base;

class carrier_base // the base in the queue
{
public:
	using ptr = std::unique_ptr<carrier_base>;

	virtual ~carrier_base() {}

	virtual void accept(carrier_visitor_base *p_visitor) = 0;
};

template <typename T>
class carrier;

class carrier_visitor_base
{
public:
	virtual ~carrier_visitor_base() {}

	virtual void handle(carrier<ev_test> *p_evt) = 0;
	virtual void handle(carrier<char> *p_evt) = 0;
	virtual void handle(carrier<int> *p_evt) = 0;
};

template <typename T>
class carrier : public carrier_base // the specific carrier
{
public:
	explicit carrier() : m_data() {}

	explicit carrier(const T &data) : m_data(data) {}
	virtual void accept(carrier_visitor_base *p_visitor) override
	{
		p_visitor->handle(this);
	}

	T &data() { return m_data; }
private:
	T m_data;
};
} // ns net

//----------------------------------------------------------------------------
// external serialization function
//----------------------------------------------------------------------------
BOOST_SERIALIZATION_SPLIT_FREE(ev_test)
namespace boost
{
namespace serialization
{
// serialization function for carrier_base
template <class Archive>
void serialize(Archive &ar, net::carrier_base &t, const unsigned int
version)
{
}

// serialization function for net::carrier<T>
template <class Archive, typename T>
void serialize(Archive &ar, net::carrier<T> &t, const unsigned int version)
{
	ar &boost::serialization::make_nvp(
		"carrier_base",
		boost::serialization::base_object<net::carrier_base>(t));

	auto &data = t.data();
	ar &BOOST_SERIALIZATION_NVP(data);
}

// serialization function for ev_test
template <class Archive>
inline void save(Archive &ar, const ev_test &t, const unsigned int version)
{
	size_t size = t.m_data.size();
	ar &BOOST_SERIALIZATION_NVP(size);
	ar &boost::serialization::make_nvp(
		"m_data",
		boost::serialization::make_binary_object(const_cast<uint8_t*>(t.m_data.data()), t.m_data.size()));
}
template <class Archive>
inline void load(Archive &ar, ev_test &t, const unsigned int version)
{
	size_t size = 0;
	ar &BOOST_SERIALIZATION_NVP(size);
	t.m_data.resize(size);

	ar &boost::serialization::make_nvp(
		"m_data",
		boost::serialization::make_binary_object(t.m_data.data(), t.m_data.size()));
}
}
}

// we must export all carrier
BOOST_SERIALIZATION_SHARED_PTR(net::carrier<ev_test>)
BOOST_CLASS_EXPORT(net::carrier<ev_test>)

//----------------------------------------------------------------------------
// the traits for the boost serialization tests
//----------------------------------------------------------------------------
struct boost_xml_trait
{
	static const char *name() { return "boost_xml_test: ev_test: "; }

	typedef boost::archive::xml_oarchive oarchive;
	typedef boost::archive::xml_iarchive iarchive;
};

struct boost_text_trait
{
	static const char *name() { return "boost_text_test: ev_test: "; }

	typedef boost::archive::text_oarchive oarchive;
	typedef boost::archive::text_iarchive iarchive;
};

struct boost_binary_trait
{
	static const char *name() { return "boost_binary_test: ev_test: "; }

	typedef boost::archive::binary_oarchive oarchive;
	typedef boost::archive::binary_iarchive iarchive;
};

template <typename archive_trait>
struct boost_test
{
	static const char *name() { return archive_trait::name(); }
	static size_t msg_size() { return 600; }

	// throws boost::archive::archive_exception
	template <typename T>
	static net::packet to_wire(const T &data)
	{
		using namespace boost::iostreams;

		using T1 = typename std::remove_cv<T>::type;
		using BT = typename std::remove_reference<T1>::type;
		net::carrier_base::ptr p_carrier =
			std::make_unique<net::carrier<BT>>(data);

		net::packet p;
		p.reserve(msg_size());
		{
			back_insert_device<net::packet> sink(p);
			stream<back_insert_device<net::packet>> os{sink};

			typename archive_trait::oarchive oa(os);
			oa << BOOST_SERIALIZATION_NVP(p_carrier);
		}
		return p;
	}

	// throws boost::archive::archive_exception
	static net::carrier_base::ptr from_wire(const net::packet &data)
	{
		using namespace boost::iostreams;
		array_source source{data.data(), data.size()};
		stream<array_source> is{source};

		net::carrier_base::ptr p_carrier;
		typename archive_trait::iarchive ia(is); // this takes the most time
		ia >> BOOST_SERIALIZATION_NVP(p_carrier);

		return p_carrier;
	}
};

//----------------------------------------------------------------------------
// XML
//----------------------------------------------------------------------------
static void to_wire_xml(benchmark::State &state)
{
	std::locale::global(std::locale("C"));
	ev_test data(state.range_x());

	while (state.KeepRunning())
	{
		boost_test<boost_xml_trait>::to_wire(data);
	}

	state.SetBytesProcessed(static_cast<int64_t>(state.iterations()) *
							state.range_x());
}
BENCHMARK(to_wire_xml)->Range(8, range_mult << range_max_step);

static void from_wire_xml(benchmark::State &state)
{
	std::locale::global(std::locale("C"));
	auto buffer =
		boost_test<boost_xml_trait>::to_wire(ev_test(state.range_x()));

	while (state.KeepRunning())
	{
		boost_test<boost_xml_trait>::from_wire(buffer);
	}

	state.SetBytesProcessed(static_cast<int64_t>(state.iterations()) *
							state.range_x());
}
BENCHMARK(from_wire_xml)->Range(8, range_mult << range_max_step);

//----------------------------------------------------------------------------
// Text
//----------------------------------------------------------------------------
static void to_wire_text(benchmark::State &state)
{
	std::locale::global(std::locale("C"));
	ev_test data(state.range_x());

	while (state.KeepRunning())
	{
		boost_test<boost_text_trait>::to_wire(data);
	}

	state.SetBytesProcessed(static_cast<int64_t>(state.iterations()) *
							state.range_x());
}
BENCHMARK(to_wire_text)->Range(8, range_mult << range_max_step);

static void from_wire_text(benchmark::State &state)
{
	std::locale::global(std::locale("C"));
	auto buffer =
		boost_test<boost_text_trait>::to_wire(ev_test(state.range_x()));

	while (state.KeepRunning())
	{
		boost_test<boost_text_trait>::from_wire(buffer);
	}

	state.SetBytesProcessed(static_cast<int64_t>(state.iterations()) *
							state.range_x());
}
BENCHMARK(from_wire_text)->Range(8, range_mult << range_max_step);

//----------------------------------------------------------------------------
// Binary
//----------------------------------------------------------------------------
static void to_wire_binary(benchmark::State &state)
{
	std::locale::global(std::locale("C"));
	ev_test data(state.range_x());

	while (state.KeepRunning())
	{
		boost_test<boost_binary_trait>::to_wire(data);
	}

	state.SetBytesProcessed(static_cast<int64_t>(state.iterations()) *
							state.range_x());
}
BENCHMARK(to_wire_binary)->Range(8, range_mult << range_max_step);

static void from_wire_binary(benchmark::State &state)
{
	std::locale::global(std::locale("C"));

	auto buffer =
		boost_test<boost_binary_trait>::to_wire(ev_test(state.range_x()));

	while (state.KeepRunning())
	{
		boost_test<boost_binary_trait>::from_wire(buffer);
	}

	state.SetBytesProcessed(static_cast<int64_t>(state.iterations()) *
							state.range_x());
}
BENCHMARK(from_wire_binary)->Range(8, range_mult << range_max_step);

BENCHMARK_MAIN();