Skip to content

Commit

Permalink
Merge pull request #13854 from bangerth/empty
Browse files Browse the repository at this point in the history
Optimize Utilities::pack/unpack for empty objects.
  • Loading branch information
kronbichler committed May 30, 2022
2 parents baa3036 + 27fb68d commit cc45cca
Show file tree
Hide file tree
Showing 4 changed files with 176 additions and 62 deletions.
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

0 comments on commit cc45cca

Please sign in to comment.