Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optimize Utilities::pack/unpack for empty objects. #13854

Merged
merged 4 commits into from
May 30, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
96 changes: 44 additions & 52 deletions include/deal.II/base/mpi_consensus_algorithms.h
Original file line number Diff line number Diff line change
Expand Up @@ -1177,29 +1177,27 @@ namespace Utilities
{
// TODO: For the moment, simply implement this special case by
// forwarding to the other function with rewritten function
// objects and using a plain 'char' as answer type. This way,
// objects and using an empty type as answer type. This way,
// we have the interface in place and can provide a more
// efficient implementation later on.
return nbx<RequestType, char>(
using EmptyType = std::tuple<>;

return nbx<RequestType, EmptyType>(
targets,
create_request,
// answer_request:
[&process_request](const unsigned int source_rank,
const RequestType &request) -> char {
const RequestType &request) -> EmptyType {
process_request(source_rank, request);
// Return something. What it is is arbitrary here, except that
// we will want to check what it is below in the process_answer().
// We choose the smallest possible data type for the replies (a
// 'char'), but we can make ourselves feel more important by
// putting a whole " " into one char (sensible editor
// settings assumed).
return '\t';
// we want it to be as small an object as possible. Using
// std::tuple<> is interpreted as an empty object that is packed
// down to a zero-length char array.
return {};
},
// process_answer:
[](const unsigned int /*target_rank */, const char &answer) {
(void)answer;
Assert(answer == '\t', ExcInternalError());
},
[](const unsigned int /*target_rank */,
const EmptyType & /*answer*/) {},
comm);
}

Expand Down Expand Up @@ -1231,29 +1229,27 @@ namespace Utilities
{
// TODO: For the moment, simply implement this special case by
// forwarding to the other function with rewritten function
// objects and using a plain 'char' as answer type. This way,
// objects and using an empty type as answer type. This way,
// we have the interface in place and can provide a more
// efficient implementation later on.
return pex<RequestType, char>(
using EmptyType = std::tuple<>;

return pex<RequestType, EmptyType>(
targets,
create_request,
// answer_request:
[&process_request](const unsigned int source_rank,
const RequestType &request) -> char {
const RequestType &request) -> EmptyType {
process_request(source_rank, request);
// Return something. What it is is arbitrary here, except that
// we will want to check what it is below in the process_answer().
// We choose the smallest possible data type for the replies (a
// 'char'), but we can make ourselves feel more important by
// putting a whole " " into one char (sensible editor
// settings assumed).
return '\t';
// we want it to be as small an object as possible. Using
// std::tuple<> is interpreted as an empty object that is packed
// down to a zero-length char array.
return {};
},
// process_answer:
[](const unsigned int /*target_rank */, const char &answer) {
(void)answer;
Assert(answer == '\t', ExcInternalError());
},
[](const unsigned int /*target_rank */,
const EmptyType & /*answer*/) {},
comm);
}

Expand Down Expand Up @@ -1287,29 +1283,27 @@ namespace Utilities
{
// TODO: For the moment, simply implement this special case by
// forwarding to the other function with rewritten function
// objects and using a plain 'char' as answer type. This way,
// objects and using an empty type as answer type. This way,
// we have the interface in place and can provide a more
// efficient implementation later on.
return serial<RequestType, char>(
using EmptyType = std::tuple<>;

return serial<RequestType, EmptyType>(
targets,
create_request,
// answer_request:
[&process_request](const unsigned int source_rank,
const RequestType &request) -> char {
const RequestType &request) -> EmptyType {
process_request(source_rank, request);
// Return something. What it is is arbitrary here, except that
// we will want to check what it is below in the process_answer().
// We choose the smallest possible data type for the replies (a
// 'char'), but we can make ourselves feel more important by
// putting a whole " " into one char (sensible editor
// settings assumed).
return '\t';
// we want it to be as small an object as possible. Using
// std::tuple<> is interpreted as an empty object that is packed
// down to a zero-length char array.
return {};
},
// process_answer:
[](const unsigned int /*target_rank */, const char &answer) {
(void)answer;
Assert(answer == '\t', ExcInternalError());
},
[](const unsigned int /*target_rank */,
const EmptyType & /*answer*/) {},
comm);
}

Expand Down Expand Up @@ -1343,29 +1337,27 @@ namespace Utilities
{
// TODO: For the moment, simply implement this special case by
// forwarding to the other function with rewritten function
// objects and using a plain 'char' as answer type. This way,
// objects and using an empty type as answer type. This way,
// we have the interface in place and can provide a more
// efficient implementation later on.
return selector<RequestType, char>(
using EmptyType = std::tuple<>;

return selector<RequestType, EmptyType>(
targets,
create_request,
// answer_request:
[&process_request](const unsigned int source_rank,
const RequestType &request) -> char {
const RequestType &request) -> EmptyType {
process_request(source_rank, request);
// Return something. What it is is arbitrary here, except that
// we will want to check what it is below in the process_answer().
// We choose the smallest possible data type for the replies (a
// 'char'), but we can make ourselves feel more important by
// putting a whole " " into one char (sensible editor
// settings assumed).
return '\t';
// we want it to be as small an object as possible. Using
// std::tuple<> is interpreted as an empty object that is packed
// down to a zero-length char array.
return {};
},
// process_answer:
[](const unsigned int /*target_rank */, const char &answer) {
(void)answer;
Assert(answer == '\t', ExcInternalError());
},
[](const unsigned int /*target_rank */,
const EmptyType & /*answer*/) {},
comm);
}

Expand Down
78 changes: 68 additions & 10 deletions include/deal.II/base/utilities.h
Original file line number Diff line number Diff line change
Expand Up @@ -559,7 +559,7 @@ namespace Utilities
invert_permutation(const std::vector<Integer> &permutation);

/**
* Given an arbitrary object of type T, use boost::serialization utilities
* Given an arbitrary object of type `T`, use boost::serialization utilities
* to pack the object into a vector of characters and append it to the
* given buffer. The number of elements that have been added to the buffer
* will be returned. The object can be unpacked using the Utilities::unpack
Expand All @@ -572,6 +572,45 @@ namespace Utilities
* If many consecutive calls with the same buffer are considered, it is
* recommended for reasons of performance to ensure that its capacity is
* sufficient.
*
* This function considers a number of special cases for which packing (and
* unpacking) can be simplified. These are:
* - If the object of type `T` is relatively small (less than 256 bytes) and
* if `T` satisfies `std::is_trivially_copyable`, then it is copied bit
* by bit into the output buffer.
* - If no compression is requested, and if the object is a vector of objects
* whose type `T` satisfies `std::is_trivially_copyable`, then packing
* implies copying the length of the vector into the destination buffer
* followed by a bit-by-bit copy of the contents of the vector. A
* similar process is used for vectors of vectors of objects whose type
* `T` satisfies `std::is_trivially_copyable`.
* - Finally, if the type `T` of the object to be packed is std::tuple<>
* (i.e., a tuple without any elements as indicated by the empty argument
* list) and if no compression is requested, then this
* type is considered an "empty" type and it is packed
* into a zero byte buffer. Using empty types is occasionally useful when
* sending messages to other processes if the important part about the
* message is that it is *sent*, not what it *contains* -- in other words,
* it puts the receiver on notice of something, without having to provide
* any details. In such cases, it is helpful if the message body can be
* empty -- that is, have length zero -- and using std::tuple<> facilitates
* this by providing a type which the present function packs into an
* empty output buffer, given that many deal.II functions send objects
* only after calling pack() to serialize them.
*
* In several of the special cases above, the `std::is_trivially_copyable`
* property is important, see
* https://en.cppreference.com/w/cpp/types/is_trivially_copyable .
* For a type `T` to satisfy this property essentially means that an object
* `t2` of this type can be initialized by copying another object `t1`
* bit-by-bit into the memory space of `t2`. In particular, this is the case
* for built-in types such as `int`, `double`, or `char`, as well as
* structures and classes that only consist of such types and that have
* neither user-defined constructors nor `virtual` functions. In practice,
* and together with the fact that vectors and vector-of-vectors of these
* types are also special-cased, this covers many of the most common kinds of
* messages one sends around with MPI or one wants to serialize (the two
* most common use cases for this function).
*/
template <typename T>
size_t
Expand All @@ -581,7 +620,7 @@ namespace Utilities

/**
* Creates and returns a buffer solely for the given object, using the
* above mentioned pack function.
* above mentioned pack function (including all of its special cases).
*
* If the library has been compiled with ZLIB enabled, then the output buffer
* can be compressed. This can be triggered with the parameter
Expand All @@ -593,11 +632,12 @@ namespace Utilities

/**
* Given a vector of characters, obtained through a call to the function
* Utilities::pack, restore its content in an object of type T.
* Utilities::pack, restore its content in an object of type `T`.
*
* This function uses boost::serialization utilities to unpack the object
* from a vector of characters, and it is the inverse of the function
* Utilities::pack().
* Utilities::pack(). It considers the same set of special cases as
* documented with the pack() function.
*
* The @p allow_compression parameter denotes if the buffer to
* read from could have been previously compressed with ZLIB, and
Expand Down Expand Up @@ -1456,13 +1496,20 @@ namespace Utilities
if (std::is_trivially_copyable<T>() && sizeof(T) < 256)
#endif
{
// Determine the size. There are places where we would like to use a
// truly empty type, for which we use std::tuple<> (i.e., a tuple
// of zero elements). For this class, the compiler reports a nonzero
// sizeof(...) because that is the minimum possible for objects --
// objects need to have distinct addresses, so they need to have a size
// of at least one. But we can special case this situation.
size = (std::is_same<T, std::tuple<>>::value ? 0 : sizeof(T));

(void)allow_compression;
const std::size_t previous_size = dest_buffer.size();
dest_buffer.resize(previous_size + sizeof(T));
dest_buffer.resize(previous_size + size);

std::memcpy(dest_buffer.data() + previous_size, &object, sizeof(T));

size = sizeof(T);
if (size > 0)
std::memcpy(dest_buffer.data() + previous_size, &object, size);
}
// Next try if we have a vector of trivially copyable objects.
// If that is the case, we can shortcut the whole BOOST serialization
Expand Down Expand Up @@ -1536,11 +1583,22 @@ namespace Utilities
if (std::is_trivially_copyable<T>() && sizeof(T) < 256)
#endif
{
// Determine the size. There are places where we would like to use a
// truly empty type, for which we use std::tuple<> (i.e., a tuple
// of zero elements). For this class, the compiler reports a nonzero
// sizeof(...) because that is the minimum possible for objects --
// objects need to have distinct addresses, so they need to have a size
// of at least one. But we can special case this situation.
const std::size_t size =
(std::is_same<T, std::tuple<>>::value ? 0 : sizeof(T));

T object;

(void)allow_compression;
Assert(std::distance(cbegin, cend) == sizeof(T), ExcInternalError());
std::memcpy(&object, &*cbegin, sizeof(T));
Assert(std::distance(cbegin, cend) == size, ExcInternalError());

if (size > 0)
std::memcpy(&object, &*cbegin, size);

return object;
}
Expand Down
60 changes: 60 additions & 0 deletions tests/base/utilities_pack_unpack_08.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// ---------------------------------------------------------------------
//
// Copyright (C) 2017 - 2020 by the deal.II authors
//
// This file is part of the deal.II library.
//
// The deal.II library is free software; you can use it, redistribute
// it, and/or modify it under the terms of the GNU Lesser General
// Public License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
// The full text of the license can be found in the file LICENSE.md at
// the top level directory of deal.II.
//
// ---------------------------------------------------------------------


// Make sure that Utilities::pack/unpack can be used on objects that
// are empty, and that the resulting packed string has length zero.


#include <deal.II/base/point.h>
#include <deal.II/base/timer.h>
#include <deal.II/base/utilities.h>

#include <tuple>

#include "../tests.h"



void
test()
{
using Empty = std::tuple<>;

Empty e;
deallog << "Size = " << sizeof(e) << std::endl;

// Pack the object and make sure the packed length is zero
const std::vector<char> packed = Utilities::pack(e, false);
deallog << "Packed size = " << packed.size() << std::endl;

// Then unpack again. The two should be the same -- though one may
// question what equality of objects of size zero might mean -- and
// we should check so:
Empty e2 = Utilities::unpack<Empty>(packed, false);
Assert(e2 == e, ExcInternalError());

deallog << "OK" << std::endl;
}



int
main()
{
initlog();

test();
}
4 changes: 4 additions & 0 deletions tests/base/utilities_pack_unpack_08.output
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@

DEAL::Size = 1
DEAL::Packed size = 0
DEAL::OK