From 3ca9acb90a45babf9e35541cb58eb90d4fbdfd3a Mon Sep 17 00:00:00 2001 From: florijan <florijan@memgraph.io> Date: Tue, 3 Oct 2017 09:29:07 +0200 Subject: [PATCH] Copy-on-write added to POC Reviewers: buda, mislav.bradac Reviewed By: mislav.bradac Subscribers: pullbot Differential Revision: https://phabricator.memgraph.io/D848 --- poc/utils/copy_on_write.hpp | 106 ++++++++++++++++++++++++++++++ poc/utils/utils_copy_on_write.cpp | 67 +++++++++++++++++++ 2 files changed, 173 insertions(+) create mode 100644 poc/utils/copy_on_write.hpp create mode 100644 poc/utils/utils_copy_on_write.cpp diff --git a/poc/utils/copy_on_write.hpp b/poc/utils/copy_on_write.hpp new file mode 100644 index 000000000..3a3d4683d --- /dev/null +++ b/poc/utils/copy_on_write.hpp @@ -0,0 +1,106 @@ +#pragma once + +#include <memory> + +#include "utils/assert.hpp" + +namespace utils { + +/** + * Helper class for implementing copy-on-write member variables. Memory + * management is automatic via shared-ptr: it is allowed for the original + * owner to expire, the content will remain valid in copies. + * + * This class is generally not thread-safe. However, it is intended for use in + * Memgraph's MVCC which is concurrent, but with specific guarantees that also + * make it possible to use this. These guarantees are: + * 1. Writing can only happen when there are no copies (no parallel reads). An + * implication of this is that an obtained copy is immutable, even though in + * general CopyOnWrite does not guarantee this (generally the owner's + * modificatins will be visible to the copies). + * 2. Copies are not created in parallel. MVCC guarantees this with + * record-locking, but should be in fact legal in CopyOnWrite. + * + * @tparam TElement - type of content. Must be copy-constructable. + */ +template <typename TElement> +class CopyOnWrite { + public: + /** + * Creates a new CopyOnWrite that owns it's element. + * + * @param args - Arguments forwarded to the TElement constructor. + * @tparam TArgs - Argument types. + */ + template <typename... TArgs> + CopyOnWrite(TArgs &&... args) { + TElement *new_element = new TElement(std::forward<TArgs>(args)...); + element_ptr_.reset(new_element); + is_owner_ = true; + } + + /** Creates a copy of the given CopyOnWrite object that does not copy the + * element and does not assume ownership over it. */ + CopyOnWrite(const CopyOnWrite &other) + : element_ptr_{other.element_ptr_}, is_owner_{false} {} + + /** Creates a copy of the given CopyOnWrite object that does not copy the + * element and does not assume ownership over it. This is a non-const + * reference accepting copy constructor. The hack is necessary to prevent the + * variadic constructor from being a better match for non-const CopyOnWrite + * argument. */ + CopyOnWrite(CopyOnWrite &other) + : CopyOnWrite(const_cast<const CopyOnWrite &>(other)) {} + + /** Creates a copy of the given temporary CyopOnWrite object. If the temporary + * is owner then ownership is transferred to this CopyOnWrite. Otherwise this + * CopyOnWrite does not become the owner. */ + CopyOnWrite(CopyOnWrite &&other) = default; + + /** Copy assignment of another CopyOnWrite. Does not transfer ownership (this + * CopyOnWrite is not the owner). */ + CopyOnWrite &operator=(const CopyOnWrite &other) { + element_ptr_ = other.element_ptr_; + is_owner_ = false; + return *this; + } + + /** Copy assignment of a temporary CopyOnWrite. If the temporary is owner then + * ownerships is transferred to this CopyOnWrite. Otherwise this CopyOnWrite + * does not become the owner. */ + CopyOnWrite &operator=(CopyOnWrite &&other) = default; + + // All the dereferencing operators are overloaded to return a const element + // reference. There is no differentiation between const and non-const member + // function behavior because an implicit copy creation on non-const + // dereferencing would most likely result in excessive copying. For that + // reason + // an explicit call to the `Write` function is required to obtain a non-const + // reference to element. + const TElement &operator*() { return *element_ptr_; } + const TElement &operator*() const { return *element_ptr_; } + const TElement &get() { return *element_ptr_; } + const TElement &get() const { return *element_ptr_; } + const TElement *operator->() { return element_ptr_.get(); } + const TElement *operator->() const { return element_ptr_.get(); } + + /** Indicates if this CopyOnWrite object is the owner of it's element. */ + bool is_owner() const { return is_owner_; }; + + /** + * If this CopyOnWrite is the owner of it's element, a non-const reference to + * is returned. If this CopyOnWrite is not the owner, then the element is + * copied and this CopyOnWrite becomes the owner. + */ + TElement &Write() { + if (is_owner_) return *element_ptr_; + element_ptr_ = std::shared_ptr<TElement>(new TElement(*element_ptr_)); + is_owner_ = true; + return *element_ptr_; + }; + + private: + std::shared_ptr<TElement> element_ptr_; + bool is_owner_{false}; +}; +} // namespace utils diff --git a/poc/utils/utils_copy_on_write.cpp b/poc/utils/utils_copy_on_write.cpp new file mode 100644 index 000000000..856ec230e --- /dev/null +++ b/poc/utils/utils_copy_on_write.cpp @@ -0,0 +1,67 @@ +#include "gtest/gtest.h" + +#include "utils/copy_on_write.hpp" + +TEST(CopyOneWrite, Dereferencing) { + utils::CopyOnWrite<std::string> number("42"); + EXPECT_EQ((*number).size(), 2); + EXPECT_EQ(number.get().size(), 2); + EXPECT_EQ(number->size(), 2); +} + +TEST(CopyOneWrite, Ownership) { + utils::CopyOnWrite<std::string> number("42"); + EXPECT_TRUE(number.is_owner()); + + utils::CopyOnWrite<std::string> copy_constructed(number); + auto copy_assigned = number; + EXPECT_FALSE(copy_constructed.is_owner()); + EXPECT_FALSE(copy_assigned.is_owner()); + EXPECT_TRUE(number.is_owner()); +} + +TEST(CopyOneWrite, OwnershipFromTemporary) { + utils::CopyOnWrite<std::string> copy_constructed( + utils::CopyOnWrite<std::string>("42")); + auto copy_assigned = utils::CopyOnWrite<std::string>("42"); + EXPECT_TRUE(copy_constructed.is_owner()); + EXPECT_TRUE(copy_assigned.is_owner()); +} + +struct DestructorCounter { + DestructorCounter(int &counter) : counter_(counter) {} + ~DestructorCounter() { + counter_++; + } + private: + int &counter_; +}; + +TEST(CopyOnWrite, ElementDestruction) { + int counter = 0; + std::vector<utils::CopyOnWrite<DestructorCounter>> initial_owner; + initial_owner.emplace_back(counter); + { + auto copy = initial_owner[0]; + EXPECT_EQ(counter, 0); + initial_owner.clear(); + EXPECT_EQ(counter, 0); + } + EXPECT_EQ(counter, 1); +} + +TEST(CopyOneWrite, Copy) { + utils::CopyOnWrite<std::string> number("42"); + auto copy = number; + EXPECT_EQ(*copy, "42"); + + // modification by owner + number.Write().resize(1); + EXPECT_EQ(*number, "4"); + EXPECT_EQ(*copy, "4"); + + // modification by copy + copy.Write().resize(2, '3'); + EXPECT_EQ(*number, "4"); + EXPECT_EQ(*copy, "43"); +}