From 0908d3a17318c9dfcfb1aa82bbc48a5b06142226 Mon Sep 17 00:00:00 2001 From: David Aurelio Date: Fri, 31 May 2019 09:40:41 -0700 Subject: [PATCH] Data structure for exclusive writing Summary: Adds a data structure that holds a series of values that can be *borrowed* for exclusive writing. That means, that only a single consumer can write to any value owned by the data structure. In addition, the data structure exposes read access via iteration over all contained values. A typical use case would be a counter with thread-local values that are accumulated by readers in other parts of a programm. The design carefully avoids the use of atomics or locks for reading and writing. This approach avoids cache flushes and bus sync between cores. Borrowing and returning a value go through a central lock to guarantee the consistency of the underlying data structure. Values are allocated in a `std::forward_list`, which typically should avoid two values in the same cache line -- in that case, writing to one value would still cause cache flushing on other cores. An alternative approach would be to allocate values continuously on cache line boundaries (with padding between them). We can still change the code if the current approach turns out to be too naive (non-deterministic). Reviewed By: SidharthGuglani Differential Revision: D15535018 fbshipit-source-id: 212ac88bba9682a4c9d4326b46de0ee2fb5d9a7e --- util/BUCK | 30 ++++++ util/SingleWriterValueList.cpp | 32 ++++++ util/SingleWriterValueList.h | 136 ++++++++++++++++++++++++ util/SingleWriterValueListTest.cpp | 163 +++++++++++++++++++++++++++++ 4 files changed, 361 insertions(+) create mode 100644 util/BUCK create mode 100644 util/SingleWriterValueList.cpp create mode 100644 util/SingleWriterValueList.h create mode 100644 util/SingleWriterValueListTest.cpp diff --git a/util/BUCK b/util/BUCK new file mode 100644 index 00000000..5b0e9e00 --- /dev/null +++ b/util/BUCK @@ -0,0 +1,30 @@ +# Copyright (c) Facebook, Inc. and its affiliates. +# +# This source code is licensed under the MIT license found in the +# LICENSE file in the root directory of this source tree. +load("//tools/build_defs/oss:yoga_defs.bzl", "GTEST_TARGET", "LIBRARY_COMPILER_FLAGS", "yoga_cxx_library", "yoga_cxx_test") + +_TESTS = glob(["*Test.cpp"]) + +yoga_cxx_library( + name = "util", + srcs = glob( + ["*.cpp"], + exclude = _TESTS, + ), + header_namespace = "yoga/util", + exported_headers = glob(["*.h"]), + compiler_flags = LIBRARY_COMPILER_FLAGS, + tests = [":test"], + visibility = ["PUBLIC"], +) + +yoga_cxx_test( + name = "test", + srcs = _TESTS, + compiler_flags = LIBRARY_COMPILER_FLAGS, + deps = [ + ":util", + GTEST_TARGET, + ], +) diff --git a/util/SingleWriterValueList.cpp b/util/SingleWriterValueList.cpp new file mode 100644 index 00000000..753a2ad2 --- /dev/null +++ b/util/SingleWriterValueList.cpp @@ -0,0 +1,32 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the LICENSE + * file in the root directory of this source tree. + */ +#include "SingleWriterValueList.h" + +namespace facebook { +namespace yoga { +namespace detail { + +void* FreeList::getRaw() { + if (free_.size() == 0) + return nullptr; + + auto ptr = free_.top(); + free_.pop(); + return ptr; +} + +void FreeList::put(std::mutex& mutex, void* ptr) { + std::lock_guard lock{mutex}; + free_.push(ptr); +} + +FreeList::FreeList() = default; +FreeList::~FreeList() = default; + +} // namespace detail +} // namespace yoga +} // namespace facebook diff --git a/util/SingleWriterValueList.h b/util/SingleWriterValueList.h new file mode 100644 index 00000000..3207ae3c --- /dev/null +++ b/util/SingleWriterValueList.h @@ -0,0 +1,136 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the LICENSE + * file in the root directory of this source tree. + */ +#include +#include +#include +#include +#include +#include + +namespace facebook { +namespace yoga { + +namespace detail { + +class FreeList { + std::stack free_; + void* getRaw(); + +public: + FreeList(); + ~FreeList(); + + void put(std::mutex&, void*); + + template + T* get() { + return static_cast(getRaw()); + } +}; + +} // namespace detail + +/// SingleWriterValueList is a data structure that holds a list of values. Each +/// value can be borrowed for exclusive writing, and will not be exposed to +/// another borrower until returned. +/// Additionaly, the whole list of values can be accessed for reading via const +/// iterators. Read consistency depends on CPU internals, i.e. whether values +/// are written to memory atomically. +/// +/// A typical usage scenario would be a set of threads, where each thread +/// borrows a value for lock free writing, e.g. as a thread local variable. This +/// avoids the usage of atomics, or locking of shared memory, which both can +/// lead to increased latency due to CPU cache flushes and waits. +/// +/// Values are heap allocated (via forward_list), which typically will avoid +/// multiple values being allocated in the same CPU cache line, which would also +/// lead to cache flushing. +/// +/// SingleWriterValueList never deallocates, to guarantee the validity of +/// references and iterators. However, memory returned by a borrower can be +/// borrowed again. +/// +/// SingleWriterValueList supports return policies as second template parameter, +/// i.e. an optional mutation of values after a borrower returns them. The +/// default policy is to do nothing. SingleWriterValueList::resetPolicy is a +/// convenience method that will move assign the default value of a type. +/// +/// Example: +/// +/// static SingleWriterValueList counters; +/// thread_local auto localCounter = counters.borrow(); +/// +/// /* per thread */ +/// localCounter =+ n; +/// +/// /* anywhere */ +/// std::accumulate(counters.begin(), counters.end(), 0); +/// +template +class SingleWriterValueList { + std::forward_list values_{}; + std::mutex acquireMutex_{}; + detail::FreeList freeValuesList_{}; + + T* allocValue() { + values_.emplace_front(); + return &values_.front(); + } + + void returnRef(T* value) { + if (ReturnPolicy != nullptr) { + ReturnPolicy(*value); + } + freeValuesList_.put(acquireMutex_, value); + } + +public: + using const_iterator = decltype(values_.cbegin()); + + /// RAII representation of a single value, borrowed for exclusive writing. + /// Instances cannot be copied, and will return the borrowed value to the + /// owner upon destruction. + class Borrowed { + T* value_; + SingleWriterValueList* owner_; + + public: + Borrowed(T* value, SingleWriterValueList* owner) + : value_{value}, owner_{owner} {} + ~Borrowed() { + if (owner_ != nullptr && value_ != nullptr) { + owner_->returnRef(value_); + } + } + + Borrowed(Borrowed&& other) = default; + Borrowed& operator=(Borrowed&& other) = default; + + // no copies allowed + Borrowed(const Borrowed&) = delete; + Borrowed& operator=(const Borrowed&) = delete; + + T& get() { return *value_; } + T& operator*() { return get(); } + }; + + Borrowed borrow() { + std::lock_guard lock{acquireMutex_}; + T* value = freeValuesList_.get(); + return {value != nullptr ? value : allocValue(), this}; + } + + const_iterator cbegin() const { return values_.cbegin(); }; + const_iterator cend() const { return values_.cend(); }; + const_iterator begin() const { return cbegin(); }; + const_iterator end() const { return cend(); }; + + static void resetPolicy(T& value) { value = std::move(T{}); } +}; + +} // namespace yoga +} // namespace facebook diff --git a/util/SingleWriterValueListTest.cpp b/util/SingleWriterValueListTest.cpp new file mode 100644 index 00000000..c554e646 --- /dev/null +++ b/util/SingleWriterValueListTest.cpp @@ -0,0 +1,163 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the LICENSE + * file in the root directory of this source tree. + */ +#include +#include + +#include +#include +#include + +namespace facebook { +namespace yoga { + +static_assert( + !std::is_copy_constructible>::value, + "SingleWriterValueList must not be copyable"); +static_assert( + !std::is_copy_assignable>::value, + "SingleWriterValueList must not be copyable"); +static_assert( + !std::is_copy_constructible::Borrowed>::value, + "SingleWriterValueList::Borrowed must not be copyable"); +static_assert( + !std::is_copy_assignable::Borrowed>::value, + "SingleWriterValueList::Borrowed must not be copyable"); +static_assert( + std::is_move_constructible::Borrowed>::value, + "SingleWriterValueList::Borrowed must be movable"); +static_assert( + std::is_move_assignable::Borrowed>::value, + "SingleWriterValueList::Borrowed must be movable"); + +TEST(SingleWriterValueList, borrowsAreExclusive) { + SingleWriterValueList x{}; + + auto a = x.borrow(); + auto b = x.borrow(); + + ASSERT_NE(&a.get(), &b.get()); +} + +TEST(SingleWriterValueList, borrowsSupportDereference) { + SingleWriterValueList x{}; + + auto a = x.borrow(); + *a = 123; + + ASSERT_EQ(*a, 123); +} + +TEST(SingleWriterValueList, borrowsHaveGetMethod) { + SingleWriterValueList x{}; + + auto a = x.borrow(); + a.get() = 123; + + ASSERT_EQ(a.get(), 123); +} + +TEST(SingleWriterValueList, exposesBorrowsViaIterator) { + SingleWriterValueList x{}; + + auto a = x.borrow(); + auto b = x.borrow(); + + *a = 12; + *b = 34; + + int sum = 0; + for (auto& i : x) { + sum += i; + } + ASSERT_EQ(sum, 12 + 34); +} + +TEST(SingleWriterValueList, exposesBorrowsViaConstIterator) { + SingleWriterValueList x{}; + + auto a = x.borrow(); + auto b = x.borrow(); + + *a = 12; + *b = 34; + + ASSERT_EQ(std::accumulate(x.cbegin(), x.cend(), 0), 12 + 34); +} + +TEST(SingleWriterValueList, doesNotDeallocateReturnedBorrows) { + SingleWriterValueList x{}; + + std::unordered_set values; + { + auto a = x.borrow(); + auto b = x.borrow(); + values.insert(&a.get()); + values.insert(&b.get()); + } + + auto it = x.begin(); + + ASSERT_NE(it, x.end()); + ASSERT_NE(values.find(&*it), values.end()); + + ASSERT_NE(++it, x.end()); + ASSERT_NE(values.find(&*it), values.end()); +} + +TEST(SingleWriterValueList, reusesReturnedBorrows) { + SingleWriterValueList x{}; + + int* firstBorrow; + { + auto a = x.borrow(); + firstBorrow = &a.get(); + } + + auto b = x.borrow(); + + ASSERT_EQ(&b.get(), firstBorrow); +} + +TEST(SingleWriterValueList, keepsValuesAfterReturning) { + SingleWriterValueList x{}; + + { + auto a = x.borrow(); + *a = 123; + } + + ASSERT_EQ(*x.begin(), 123); +} + +static void addOne(int& v) { + v += 1; +} + +TEST(SingleWriterValueList, allowsCustomReturnPolicy) { + SingleWriterValueList x{}; + + { + auto a = x.borrow(); + *a = 123; + } + + ASSERT_EQ(*x.begin(), 124); +} + +TEST(SingleWriterValueList, hasConvenienceResetPolicy) { + SingleWriterValueList::resetPolicy> x{}; + + { + auto a = x.borrow(); + *a = 123; + } + + ASSERT_EQ(*x.begin(), 0); +} + +} // namespace yoga +} // namespace facebook