diff --git a/src/utils/include/utils/serialization/memcpy_archive.hpp b/src/utils/include/utils/serialization/memcpy_archive.hpp new file mode 100644 index 00000000000..9a98d8cd45f --- /dev/null +++ b/src/utils/include/utils/serialization/memcpy_archive.hpp @@ -0,0 +1,231 @@ +/* + * Copyright (C) 2019 The ESPResSo project + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#ifndef ESPRESSO_MEMCPY_ARCHIVE_HPP +#define ESPRESSO_MEMCPY_ARCHIVE_HPP + +#include + +#include +#include +#include + +#include + +namespace Utils { +/** @brief Type trait to indicate that a type is + * serializable with a static size, e.g. is + * suitable for memcpy serialization. Only + * specialize this to std::true_type if it is + * guarantueed that serializing this type always + * returns the same number of bytes, independent + * of object state. + * + * @tparam T type under consideration. + */ +template struct is_statically_serializable : std::false_type {}; + +namespace detail { +/* Use serialize function only if the type is opt-in but not + * trivially copyable, in which case memcpy is more efficient. */ +template +using use_serialize = + std::integral_constant::value and + is_statically_serializable::value>; + +struct SizeArchive { + using is_saving = boost::mpl::true_; + using is_loading = boost::mpl::false_; + + size_t size = {}; + + template + constexpr auto operator<<(T const &) + -> std::enable_if_t::value> { + size += sizeof(T); + } + + template + constexpr auto operator<<(T &v) + -> std::enable_if_t::value> { + boost::serialization::serialize(*this, v, 0); + } + + template constexpr auto operator<<(boost::optional &) { + operator<<(bool()); + operator<<(T{}); + } + + template constexpr SizeArchive &operator&(T &t) { + operator<<(t); + + return *this; + } +}; + +class BasicMemcpyArchive { + /** Buffer to write to */ + Utils::Span buf; + /** Current position in the buffer */ + char *insert; + +public: + explicit BasicMemcpyArchive(Utils::Span buf) + : buf(buf), insert(buf.data()) {} + + size_t bytes_processed() const { return insert - buf.data(); } + + template + auto operator>>(T &value) + -> std::enable_if_t::value> { + /* check that there is enough space left in the buffer */ + assert((insert + sizeof(T)) <= buf.end()); + + std::memcpy(&value, insert, sizeof(T)); + insert += sizeof(T); + } + + template + auto operator<<(T const &value) + -> std::enable_if_t::value> { + /* check that there is enough space left in the buffer */ + assert((insert + sizeof(T)) <= buf.end()); + std::memcpy(insert, &value, sizeof(T)); + insert += sizeof(T); + } + + /** + * @brief Determine the static packing size of a type. + * @tparam T Type to consider. + * @return Packed size in bytes. + */ + template static size_t packing_size() { + detail::SizeArchive sa{}; + T t; + return (sa & t).size; + } +}; +} // namespace detail + +/** + * @brief Archive that deserializes from a buffer via memcpy. + * + * Can only process types that have a static serialization size, + * e.g. that serialize to the same number of bytes independent of + * the state of the object. This can either be automatically detected + * for types that are trivially copyable, or by explicitly assuring + * this by specializing @c is_statically_serializable to std::true_type. + */ +class MemcpyIArchive : public detail::BasicMemcpyArchive { +private: + using base_type = detail::BasicMemcpyArchive; + +public: + using is_loading = boost::mpl::true_; + using is_saving = boost::mpl::false_; + + /** + * @param buf Buffer to read from. + */ + explicit MemcpyIArchive(Utils::Span buf) + : detail::BasicMemcpyArchive(buf) {} + + /** + * @brief Number of bytes read from the buffer. + * @return Number of bytes read. + */ + size_t bytes_read() const { return bytes_processed(); } + + /** @copydoc base_type::packing_size */ + using base_type::packing_size; + using base_type::operator>>; + + template + auto operator>>(T &value) + -> std::enable_if_t::value> { + boost::serialization::serialize(*this, value, 0); + } + + template void operator>>(boost::optional &o) { + bool initialized{}; + operator>>(initialized); + T val{}; + operator>>(val); + + if (initialized) { + o = val; + } else { + o = boost::none; + } + } + + template MemcpyIArchive &operator&(T &value) { + operator>>(value); + + return *this; + } +}; + +/** + * @brief Archive that serializes to a buffer via memcpy. + * + * @copydetails MemcpyIArchive + */ +class MemcpyOArchive : public detail::BasicMemcpyArchive { + using base_type = detail::BasicMemcpyArchive; + +public: + using is_loading = boost::mpl::false_; + using is_saving = boost::mpl::true_; + + /** + * @param buf Buffer to write to. + */ + explicit MemcpyOArchive(Utils::Span buf) + : detail::BasicMemcpyArchive(buf) {} + + /** + * @brief Number of bytes written to the buffer. + * @return Number of bytes written. + */ + size_t bytes_written() const { return bytes_processed(); } + + /** @copydoc base_type::packing_size */ + using base_type::packing_size; + using base_type::operator<<; + + template + auto operator<<(T &value) + -> std::enable_if_t::value> { + boost::serialization::serialize(*this, value, 0); + } + + template void operator<<(boost::optional &o) { + operator<<(o.is_initialized()); + operator<<(o.value_or(T{})); + } + + template MemcpyOArchive &operator&(T &value) { + operator<<(value); + + return *this; + } +}; +} // namespace Utils + +#endif // ESPRESSO_MEMCPY_ARCHIVE_HPP diff --git a/src/utils/tests/CMakeLists.txt b/src/utils/tests/CMakeLists.txt index 2fbe6cbd52e..d3c7380ce3e 100644 --- a/src/utils/tests/CMakeLists.txt +++ b/src/utils/tests/CMakeLists.txt @@ -40,6 +40,7 @@ unit_test(NAME quaternion_test SRC quaternion_test.cpp DEPENDS utils) unit_test(NAME mask_test SRC mask_test.cpp DEPENDS utils) unit_test(NAME type_traits_test SRC type_traits_test.cpp DEPENDS utils) unit_test(NAME uniform_test SRC uniform_test.cpp DEPENDS utils) +unit_test(NAME memcpy_archive_test SRC memcpy_archive_test.cpp DEPENDS utils) unit_test(NAME gather_buffer_test SRC gather_buffer_test.cpp DEPENDS utils Boost::mpi MPI::MPI_CXX NUM_PROC 4) unit_test(NAME scatter_buffer_test SRC scatter_buffer_test.cpp DEPENDS utils Boost::mpi MPI::MPI_CXX NUM_PROC 4) diff --git a/src/utils/tests/memcpy_archive_test.cpp b/src/utils/tests/memcpy_archive_test.cpp new file mode 100644 index 00000000000..4a0441e0d9d --- /dev/null +++ b/src/utils/tests/memcpy_archive_test.cpp @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2019 The ESPResSo project + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#define BOOST_TEST_MODULE memcpy archive test +#define BOOST_TEST_DYN_LINK +#include + +#include "utils/serialization/memcpy_archive.hpp" + +#include +#include + +#include + +struct NonTrivial { + boost::optional ov; + + template void serialize(Archive &ar, long int) { ar &ov; } +}; + +using OpVec = boost::optional; + +namespace Utils { +template <> struct is_statically_serializable : std::true_type {}; +} // namespace Utils + +BOOST_AUTO_TEST_CASE(packing_size_test) { + BOOST_CHECK_EQUAL(Utils::MemcpyIArchive::packing_size(), sizeof(int)); + + { + BOOST_CHECK_EQUAL(Utils::MemcpyIArchive::packing_size(), + Utils::MemcpyIArchive::packing_size()); + } +} + +BOOST_AUTO_TEST_CASE(optional_packing) { + std::vector buf(2 * Utils::MemcpyOArchive::packing_size()); + + const OpVec active = Utils::Vector3d{1., 2., 3.}; + const OpVec inactive = boost::none; + { + auto oa = Utils::MemcpyOArchive{Utils::make_span(buf)}; + auto in1 = active; + auto in2 = inactive; + oa << in1; + oa << in2; + + BOOST_CHECK_EQUAL(oa.bytes_written(), buf.size()); + } + + { + auto ia = Utils::MemcpyIArchive{Utils::make_span(buf)}; + OpVec out1, out2; + ia >> out1; + ia >> out2; + + BOOST_CHECK_EQUAL(ia.bytes_read(), buf.size()); + + BOOST_CHECK(out1 == active); + BOOST_CHECK(out2 == inactive); + } +} + +BOOST_AUTO_TEST_CASE(non_trivial_packing) { + static_assert(not std::is_trivially_copyable::value, ""); + + std::vector buf(2 * Utils::MemcpyOArchive::packing_size()); + + auto const active = NonTrivial{Utils::Vector3d{1., 2., 3.}}; + auto const inactive = NonTrivial{boost::none}; + + { + auto oa = Utils::MemcpyOArchive{Utils::make_span(buf)}; + auto in1 = active; + auto in2 = inactive; + oa << in1; + oa << in2; + + BOOST_CHECK_EQUAL(oa.bytes_written(), buf.size()); + } + + { + auto ia = Utils::MemcpyIArchive{Utils::make_span(buf)}; + NonTrivial out1, out2; + ia >> out1; + ia >> out2; + + BOOST_CHECK_EQUAL(ia.bytes_read(), buf.size()); + + BOOST_CHECK(out1.ov == active.ov); + BOOST_CHECK(out2.ov == inactive.ov); + } +} \ No newline at end of file