I have a large graph (100k nodes), in which each node has to store a bit of information per outgoing edge. Instead of keeping this in a std::vector<bool>, I'm using dynamic_bitset from Boost 1.58 for the ability to perform bitwise operations. Each node also keeps a pointer to some polymorphic object. A minimal example looks like this,
struct Node
{
std::vector<size_t> succ;
boost::dynamic_bitset<> succ_flags;
std::unique_ptr<Object> data;
};
Consider this simple benchmark program, which creates and destroys a graph:
#include <vector>
#include <boost/dynamic_bitset.hpp>
#include <memory>
constexpr int N = 50000;
struct Node
{
std::vector<size_t> succ;
boost::dynamic_bitset<> succ_flags;
//std::unique_ptr<int> data;
Node(int i)
{
for (int j = i; j < N; j += i) succ.emplace_back(j);
succ_flags.resize(succ.size());
}
};
int main()
{
std::vector<Node> nodes;
for (int i = 1; i <= N; i++) nodes.emplace_back(i);
return 0;
}
Running under the time command, a typical result is
real 0m0.055s
user 0m0.043s
sys 0m0.010s
However, uncommenting the unique_ptr line gives something more like
real 0m0.017s
user 0m0.010s
sys 0m0.003s
Conclusion: making Node heavier by adding a std::unique_ptr data member causes std::vector to perform over 3x faster!
Why is this happening, and what sort of gotcha is at work here?
Adding the unique_ptr data member makes Node a move-only type and the vector is forced to move the existing elements when growing its size. In your original example, the vector is copying the existing elements because the default move constructor definition is not noexcept (because the dynamic_bitset move constructor is not noexcept).
You should be able to reproduce the quicker result without adding the unique_ptr in one of the following two ways:
reserve room in the vector
std::vector<Node> nodes;
nodes.reserve(N);
or define your own noexcept move constructor for Node
Node(Node&& other) noexcept
: succ(std::move(other.succ))
, succ_flags(std::move(other.succ_flags))
{}
Note that if you provide the above move constructor and dynamic_bitset's move constructor actually throws an exception, std::terminate will be called.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With