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