├── lf ├── epilog.inc ├── prolog.inc ├── utility.hpp ├── memory.hpp ├── stack.hpp ├── allocator.hpp └── queue.hpp ├── test ├── unit_test │ ├── unit_test.cpp │ ├── utility.cpp │ ├── stack.cpp │ ├── memory.cpp │ ├── test.hpp │ └── allocator.cpp └── perf_test │ ├── libtag.hpp │ ├── stack.cpp │ ├── simulator2.hpp │ ├── utility.hpp │ └── cli.hpp ├── .gitignore ├── .codecov.yml ├── .travis.yml ├── doc ├── README.md └── lf │ ├── utility.md │ ├── stack.md │ ├── allocator.md │ ├── split_ref.md │ └── memory.md ├── .travis.sh └── README.md /lf/epilog.inc: -------------------------------------------------------------------------------- 1 | } // namespace lf 2 | -------------------------------------------------------------------------------- /test/unit_test/unit_test.cpp: -------------------------------------------------------------------------------- 1 | #define CATCH_CONFIG_MAIN 2 | #include "test.hpp" 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | bin/ 3 | notes.txt 4 | scratch* 5 | test/perf_test/boost/ 6 | -------------------------------------------------------------------------------- /lf/prolog.inc: -------------------------------------------------------------------------------- 1 | #if __cplusplus < 201703L 2 | #error "Requires C++17." 3 | #endif 4 | 5 | namespace lf { 6 | -------------------------------------------------------------------------------- /.codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | ignore: test/**/* 3 | range: 60..90 4 | round: nearest 5 | status: 6 | project: 7 | default: 8 | target: 80% 9 | patch: 10 | default: 11 | target: 80% 12 | 13 | comment: 14 | layout: diff 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | notifications: 2 | email: false 3 | language: cpp 4 | 5 | matrix: 6 | include: 7 | - os: linux 8 | dist: trusty 9 | sudo: true 10 | compiler: gcc 11 | env: VER=7 CODECOV=1 12 | - os: osx 13 | osx_image: xcode9.3 14 | compiler: clang 15 | env: VER=6.0 16 | 17 | script: 18 | - source .travis.sh &>/dev/null 19 | - $BUILD --version 20 | 21 | - $BUILD $DEBUG -o unit_test $UNIT_TEST/*.cpp 22 | - ./unit_test 23 | - if [ "$CODECOV" = 1 ]; then { $COV *.gcno && bash <(curl -s https://codecov.io/bash); } &>/dev/null; fi 24 | 25 | - $BUILD $PERF -o perf_test_stack $PERF_TEST/stack.cpp 26 | -------------------------------------------------------------------------------- /test/perf_test/libtag.hpp: -------------------------------------------------------------------------------- 1 | #ifndef LIBTAG_HPP 2 | #define LIBTAG_HPP 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | enum struct lib { 9 | lf, 10 | boost 11 | }; 12 | 13 | inline 14 | std::string to_str(lib tag) { 15 | return tag == lib::lf ? "lf" : "boost"; 16 | } 17 | 18 | inline 19 | std::ostream& operator<<(std::ostream& os, lib tag) { 20 | return os << to_str(tag); 21 | } 22 | 23 | inline 24 | std::istream& operator>>(std::istream& is, lib& tag) { 25 | std::string s; 26 | if (is >> s) { 27 | if (s == "lf") tag = lib::lf; 28 | else if (s == "boost") tag = lib::boost; 29 | else is.setstate(is.failbit); 30 | } 31 | return is; 32 | } 33 | 34 | #endif // LIBTAG_HPP 35 | -------------------------------------------------------------------------------- /lf/utility.hpp: -------------------------------------------------------------------------------- 1 | #ifndef LF_UTILITY_HPP 2 | #define LF_UTILITY_HPP 3 | 4 | #include 5 | #include 6 | 7 | #include "prolog.inc" 8 | 9 | inline constexpr auto rlx = std::memory_order_relaxed; 10 | inline constexpr auto rel = std::memory_order_release; 11 | inline constexpr auto acq = std::memory_order_acquire; 12 | inline constexpr auto eat = std::memory_order_consume; 13 | inline constexpr auto cst = std::memory_order_seq_cst; 14 | inline constexpr auto acq_rel = std::memory_order_acq_rel; 15 | 16 | inline constexpr auto null = std::uint32_t(-1); 17 | 18 | struct cp_t { 19 | std::uint32_t ptr{null}; 20 | std::uint32_t cnt{}; 21 | }; 22 | 23 | static_assert(std::atomic::is_always_lock_free); 24 | 25 | #include "epilog.inc" 26 | 27 | #endif // LF_UTILITY_HPP 28 | -------------------------------------------------------------------------------- /doc/README.md: -------------------------------------------------------------------------------- 1 | ## Reference 2 | 3 | ### Conventions 4 | 5 | - Constructors/destructors are not thread-safe. 6 | The destructing thread should be the last and only referencing thread. 7 | - Components in header, say `lf/foo/bar.hpp`, reside in namespace `lf::foo`. 8 | The top-level namespace `lf` is omitted in this Reference for brevity. 9 | - Concerning exception safety, assumes that move and 10 | swap operations do not throw. 11 | 12 | The library provides ready-to-use lock-free [data structures](#data-structures) 13 | as well as low-level [utilities](#utilities) to help build your own. 14 | 15 | ### Data Structures 16 | 17 | - [Stack](lf/stack.md#header-lfstackhpp) 18 | 19 | ### Utilities 20 | 21 | - [Memory](lf/memory.md#header-lfmemoryhpp) 22 | - [Split Reference Counts](lf/split_ref.md#header-lfsplit_refhpp) 23 | - [Utility](lf/utility.md#header-lfutilityhpp) 24 | -------------------------------------------------------------------------------- /.travis.sh: -------------------------------------------------------------------------------- 1 | case $TRAVIS_OS_NAME in 2 | linux) 3 | BUILD=$CXX-$VER 4 | COV=gcov-$VER 5 | EXTRA=-pthread 6 | sudo add-apt-repository ppa:ubuntu-toolchain-r/test -y 7 | sudo apt-get update -qq 8 | sudo apt-get install $CXX-$VER -yq 9 | ;; 10 | osx) 11 | BUILD=$CXX-mp-$VER 12 | COV="llvm-cov-mp-$VER gcov" 13 | export COLUMNS=80 14 | curl -LO https://raw.githubusercontent.com/GiovanniBussi/macports-ci/master/macports-ci 15 | chmod +x ./macports-ci 16 | ./macports-ci install 17 | PATH="/opt/local/bin:$PATH" 18 | sudo port install $CC-$VER 19 | ;; 20 | esac 21 | 22 | ROOT=$(pwd) 23 | UNIT_TEST=$ROOT/test/unit_test 24 | PERF_TEST=$ROOT/test/perf_test 25 | 26 | curl -LO https://dl.bintray.com/boostorg/release/1.67.0/source/boost_1_67_0.tar.gz 27 | tar --gzip -xf boost* 28 | cp -R boost*/boost $PERF_TEST/boost 29 | 30 | ESSENTIAL="-std=c++17 -Werror -pedantic -pedantic-errors -Wall -Wextra $EXTRA" 31 | DEBUG="$ESSENTIAL -O0 -g3 -coverage" 32 | PERF="$ESSENTIAL -I$ROOT -I$PERF_TEST -O3" 33 | 34 | mkdir bin 35 | cd bin 36 | -------------------------------------------------------------------------------- /doc/lf/utility.md: -------------------------------------------------------------------------------- 1 | ## Header `lf/utility.hpp` 2 | 3 | This header provides miscellaneous utilities. 4 | 5 | - [Synopsis](#synopsis) 6 | - [Details](#details) 7 | 8 | ### Synopsis 9 | 10 | ~~~C++ 11 | // Expands arguments and concatenates 12 | LF_C(a, ...) 13 | 14 | // Expands to a prefixed line # based name. 15 | LF_UNI_NAME(prefix) 16 | 17 | // Shorthand of memory order semantics 18 | inline constexpr auto rlx = std::memory_order_relaxed; 19 | inline constexpr auto rel = std::memory_order_release; 20 | inline constexpr auto acq = std::memory_order_acquire; 21 | inline constexpr auto eat = std::memory_order_consume; 22 | inline constexpr auto cst = std::memory_order_seq_cst; 23 | inline constexpr auto acq_rel = std::memory_order_acq_rel; 24 | 25 | // Checks range validity and gets its size. 26 | template 27 | std::size_t range_extent(InIt first, InIt last); 28 | ~~~ 29 | 30 | ### Details 31 | 32 | ~~~C++ 33 | template 34 | std::size_t range_extent(InIt first, InIt last); 35 | ~~~ 36 | 37 | If `first`, `last` do not specify a valid range, 38 | throws `std::invalid_argument`. 39 | Otherwise, returns size of the range. 40 | -------------------------------------------------------------------------------- /test/unit_test/utility.cpp: -------------------------------------------------------------------------------- 1 | #include "../../lf/utility.hpp" 2 | #include "../../lf/utility.hpp" 3 | 4 | #include "test.hpp" 5 | 6 | TEST_CASE("utility") { 7 | SECTION("memory order shorthand") { 8 | REQUIRE_SAME_T(decltype(lf::rlx), const std::memory_order); 9 | REQUIRE_SAME_T(decltype(lf::rel), const std::memory_order); 10 | REQUIRE_SAME_T(decltype(lf::acq), const std::memory_order); 11 | REQUIRE_SAME_T(decltype(lf::eat), const std::memory_order); 12 | REQUIRE_SAME_T(decltype(lf::cst), const std::memory_order); 13 | REQUIRE_SAME_T(decltype(lf::acq_rel), const std::memory_order); 14 | static_assert(lf::rlx == std::memory_order_relaxed); 15 | static_assert(lf::rel == std::memory_order_release); 16 | static_assert(lf::acq == std::memory_order_acquire); 17 | static_assert(lf::eat == std::memory_order_consume); 18 | static_assert(lf::cst == std::memory_order_seq_cst); 19 | static_assert(lf::acq_rel == std::memory_order_acq_rel); 20 | } 21 | SECTION("cp_t") { 22 | auto zero = lf::null + 1; 23 | REQUIRE(zero == 0); 24 | lf::cp_t cp; 25 | REQUIRE(cp.ptr == lf::null); 26 | REQUIRE(cp.cnt == 0); 27 | REQUIRE(std::atomic(cp).is_lock_free()); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /test/perf_test/stack.cpp: -------------------------------------------------------------------------------- 1 | #include "cli.hpp" 2 | #include "libtag.hpp" 3 | #include "simulator2.hpp" 4 | 5 | #include 6 | #include 7 | 8 | auto val = 0u; 9 | 10 | std::vector get_lf_fn(std::uint8_t thread_cnt) { 11 | static lf::stack stk(1_K * thread_cnt * 2); 12 | for (std::size_t i = 0; i < 1_K * thread_cnt; ++i) { 13 | stk.try_push(i); 14 | } 15 | return { 16 | []() noexcept { 17 | (void)stk.try_push(std::move(val)); 18 | }, 19 | []() noexcept { 20 | (void)stk.try_pop(); 21 | } 22 | }; 23 | } 24 | 25 | std::vector get_boost_fn(std::uint8_t thread_cnt) { 26 | static boost::lockfree::stack stk(1_K * thread_cnt * 2); 27 | for (std::size_t i = 0; i < 1_K * thread_cnt; ++i) { 28 | stk.push(i); 29 | } 30 | return { 31 | [] { 32 | (void)stk.push(val); 33 | }, 34 | [] { 35 | unsigned ret; 36 | (void)stk.pop(ret); 37 | } 38 | }; 39 | } 40 | 41 | MAIN( 42 | lib tag, 43 | unsigned thread_cnt, 44 | optional mins) { 45 | auto get_fn = tag == lib::lf ? &get_lf_fn : &get_boost_fn; 46 | simulator2::configure(thread_cnt, std::chrono::minutes(mins), get_fn(thread_cnt)); 47 | simulator2::kickoff(); 48 | simulator2::print_results(); 49 | } 50 | -------------------------------------------------------------------------------- /test/unit_test/stack.cpp: -------------------------------------------------------------------------------- 1 | #include "../../lf/stack.hpp" 2 | #include "../../lf/stack.hpp" 3 | 4 | #include "test.hpp" 5 | 6 | using ci_t = counted; 7 | 8 | namespace { 9 | 10 | void require_capacity_2(lf::stack& stk) { 11 | REQUIRE_FALSE(stk.try_pop()); 12 | REQUIRE(stk.try_push(ci_t(1))); 13 | REQUIRE(stk.try_push(ci_t(2))); 14 | REQUIRE_FALSE(stk.try_push(ci_t(3))); 15 | REQUIRE(ci_t::inst_cnt == 2); 16 | REQUIRE(stk.try_pop().value().cnt == 2); 17 | REQUIRE(stk.try_pop().value().cnt == 1); 18 | REQUIRE_FALSE(stk.try_pop()); 19 | REQUIRE(ci_t::inst_cnt == 0); 20 | } 21 | 22 | } // unnamed namespace 23 | 24 | TEST_CASE("stack") { 25 | SECTION("ctor/dtor") { 26 | lf::stack s1, s2(0), s3(2); 27 | REQUIRE(ci_t::inst_cnt == 0); 28 | REQUIRE_FALSE(s1.try_pop()); 29 | REQUIRE_FALSE(s2.try_pop()); 30 | require_capacity_2(s3); 31 | { 32 | lf::stack s(2); 33 | REQUIRE(s.try_push(ci_t(1))); 34 | REQUIRE(s.try_push(ci_t(2))); 35 | REQUIRE(ci_t::inst_cnt == 2); 36 | } 37 | REQUIRE(ci_t::inst_cnt == 0); 38 | } 39 | SECTION("reset") { 40 | lf::stack s; 41 | REQUIRE_FALSE(s.try_push(ci_t(1))); 42 | s.reset(2); 43 | require_capacity_2(s); 44 | REQUIRE(s.try_push(ci_t(1))); 45 | REQUIRE(s.try_push(ci_t(2))); 46 | REQUIRE(ci_t::inst_cnt == 2); 47 | s.reset(0); 48 | REQUIRE(ci_t::inst_cnt == 0); 49 | REQUIRE_FALSE(s.try_push(ci_t(1))); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /lf/memory.hpp: -------------------------------------------------------------------------------- 1 | #ifndef LF_MEMORY_HPP 2 | #define LF_MEMORY_HPP 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "prolog.inc" 10 | 11 | namespace impl { 12 | 13 | template 14 | struct paren_initable: std::false_type {}; 15 | 16 | template 17 | struct paren_initable()...)), T, Us...> 18 | : std::true_type {}; 19 | 20 | template 21 | inline constexpr bool paren_initable_v = paren_initable::value; 22 | 23 | } // namespace impl 24 | 25 | template 26 | T* allocate(std::size_t n = 1) { 27 | return n ? (T*)operator new(sizeof(T) * n) : (T*)nullptr; 28 | } 29 | 30 | inline 31 | void deallocate(void* p) noexcept { 32 | operator delete(p); 33 | } 34 | 35 | template 36 | void init(T*& p, Args&&... args) { 37 | if constexpr (impl::paren_initable_v) { 38 | p = new(p) T(std::forward(args)...); 39 | } 40 | else { 41 | p = new(p) T{std::forward(args)...}; 42 | } 43 | } 44 | 45 | template 46 | void init(T*&& p, Args&&... args) { 47 | if constexpr (impl::paren_initable_v) { 48 | (void)new(p) T(std::forward(args)...); 49 | } 50 | else { 51 | (void)new(p) T{std::forward(args)...}; 52 | } 53 | } 54 | 55 | template 56 | void uninit(T* p) noexcept { 57 | p->~T(); 58 | } 59 | 60 | #include "epilog.inc" 61 | 62 | #endif // LF_MEMORY_HPP 63 | -------------------------------------------------------------------------------- /doc/lf/stack.md: -------------------------------------------------------------------------------- 1 | ## Header `lf/stack.hpp` 2 | 3 | This header provides a general stack data structure. 4 | 5 | - [Synopsis](#synopsis) 6 | - [Details](#details) 7 | 8 | ### Synopsis 9 | 10 | ~~~C++ 11 | template 12 | class stack { 13 | static_assert(std::is_move_constructible_v); 14 | 15 | public: 16 | stack() noexcept; 17 | 18 | template 19 | stack(BiIt first, BiIt last); 20 | 21 | stack(const stack&) = delete; 22 | stack& operator=(const stack&) = delete; 23 | 24 | template 25 | void emplace(Us&&... us); 26 | 27 | template 28 | void bulk_push(BiIt first, BiIt last); 29 | 30 | std::optional try_pop() noexcept; 31 | }; 32 | ~~~ 33 | 34 | ### Details 35 | 36 | ~~~C++ 37 | template 38 | stack(BiIt first, BiIt last); 39 | ~~~ 40 | 41 | `BiIt` models [BidirectionalIterator][1]. 42 | Pushes elements in the range [`first`, `last`) in turn. 43 | The last-pushed element is the new stack top. 44 | Provides strong exception safety guarantee. 45 | 46 | -------------------------------------------------------------------------------- 47 | 48 | ~~~C++ 49 | template 50 | void emplace(Us&&... us); 51 | ~~~ 52 | 53 | Pushes a new element [intro-initialized](memory.md#introspective-initialization) from `us...`. 54 | Strong exception safe if the initialization is strong exception safe. 55 | 56 | -------------------------------------------------------------------------------- 57 | 58 | ~~~C++ 59 | template 60 | void bulk_push(BiIt first, BiIt last); 61 | ~~~ 62 | 63 | `BiIt` models [BidirectionalIterator][1]. 64 | Pushes elements in the range [`first`, `last`) in turn. 65 | The last-pushed element is the new stack top. 66 | Provides strong exception safety guarantee. 67 | 68 | [1]:http://en.cppreference.com/w/cpp/concept/BidirectionalIterator 69 | -------------------------------------------------------------------------------- /lf/stack.hpp: -------------------------------------------------------------------------------- 1 | #ifndef LF_STACK_HPP 2 | #define LF_STACK_HPP 3 | 4 | #include "allocator.hpp" 5 | 6 | #include 7 | 8 | #include "prolog.inc" 9 | 10 | template 11 | class stack { 12 | static_assert(std::is_move_constructible_v); 13 | 14 | public: 15 | stack() noexcept = default; 16 | 17 | explicit stack(std::uint32_t capacity): 18 | alloc(capacity) { 19 | // nop 20 | } 21 | 22 | ~stack() { 23 | uninit(); 24 | } 25 | 26 | stack(const stack&) = delete; 27 | stack& operator=(const stack&) = delete; 28 | 29 | void reset(std::uint32_t capacity) { 30 | alloc.reset(capacity, &stack::uninit, this); 31 | head.store({}, rlx); 32 | } 33 | 34 | bool try_push(T&& v) noexcept { 35 | auto p = alloc.try_allocate(); 36 | if (p == null) return false; 37 | auto& nod = alloc.deref(p); 38 | init(&nod.val, std::move(v)); 39 | cp_t newhd{p}, oldhd(head.load(rlx)); 40 | do { 41 | nod.next.store(oldhd.ptr, rlx); 42 | newhd.cnt = oldhd.cnt; 43 | } 44 | while (!head.compare_exchange_weak(oldhd, newhd, rel, rlx)); 45 | return true; 46 | } 47 | 48 | std::optional try_pop() noexcept { 49 | cp_t newhd, oldhd(head.load(acq)); 50 | node* p; 51 | do { 52 | if (oldhd.ptr == null) return {}; 53 | p = &alloc.deref(oldhd.ptr); 54 | newhd.ptr = p->next.load(rlx); 55 | newhd.cnt = oldhd.cnt + 1; 56 | } 57 | while (!head.compare_exchange_weak(oldhd, newhd, rlx, acq)); 58 | auto res = std::make_optional(std::move(p->val)); 59 | alloc.del(oldhd.ptr); 60 | return res; 61 | } 62 | 63 | private: 64 | using node = typename allocator::node; 65 | 66 | void uninit() noexcept { 67 | auto p = head.load(rlx).ptr; 68 | while (p != null) { 69 | auto& nod = alloc.deref(p); 70 | lf::uninit(&nod.val); 71 | p = nod.next.load(rlx); 72 | } 73 | } 74 | 75 | allocator alloc; 76 | std::atomic head{cp_t{}}; 77 | }; 78 | 79 | #include "epilog.inc" 80 | 81 | #endif // LF_STACK_HPP 82 | -------------------------------------------------------------------------------- /test/unit_test/memory.cpp: -------------------------------------------------------------------------------- 1 | #include "../../lf/memory.hpp" 2 | #include "../../lf/memory.hpp" 3 | 4 | #include "test.hpp" 5 | 6 | #include 7 | #include 8 | 9 | using ci_t = counted; 10 | using veci_t = std::vector; 11 | 12 | namespace { 13 | 14 | struct list_init { 15 | explicit list_init(std::initializer_list) noexcept { 16 | // nop 17 | } 18 | }; 19 | 20 | struct stru { 21 | triple tri; 22 | veci_t vec; 23 | list_init lst; 24 | ci_t ci; 25 | }; 26 | 27 | void require(const triple& tri, const veci_t& vec, const ci_t& ci) { 28 | REQUIRE(memcmp(tri, triple{1, 2, 3})); 29 | REQUIRE(vec.size() == 2); 30 | REQUIRE(vec[0] == 1); 31 | REQUIRE(vec[1] == 1); 32 | ci.require(7, 1); 33 | } 34 | 35 | } // unnamed namespace 36 | 37 | TEST_CASE("memory") { 38 | SECTION("allocate/deallocate") { 39 | REQUIRE_FALSE(lf::allocate(0)); 40 | auto p = lf::allocate(); 41 | REQUIRE(p); 42 | p[0] = 1; 43 | auto pp = lf::allocate(2); 44 | REQUIRE(pp); 45 | p[0] = 1; 46 | p[1] = 2; 47 | lf::deallocate(pp); 48 | lf::deallocate(p); 49 | lf::deallocate(nullptr); 50 | } 51 | SECTION("init/uninit") { 52 | auto p = alloc(); 53 | lf::init(&p->tri, 1, 2, 3); 54 | lf::init(&p->vec, 2, 1); 55 | lf::init(&p->lst, 1, 2, 3); 56 | lf::init(&p->ci, 7); 57 | require(p->tri, p->vec, p->ci); 58 | lf::uninit(p); 59 | REQUIRE(ci_t::inst_cnt == 0); 60 | lf::deallocate(p); 61 | auto ptri = alloc(); 62 | auto pvec = alloc(); 63 | auto plis = alloc(); 64 | auto pci = alloc(); 65 | lf::init(ptri, 1, 2, 3); 66 | lf::init(pvec, 2, 1); 67 | lf::init(plis, 1, 2, 3); 68 | lf::init(pci, 7); 69 | require(*ptri, *pvec, *pci); 70 | lf::uninit(ptri); 71 | lf::uninit(pvec); 72 | lf::uninit(plis); 73 | lf::uninit(pci); 74 | REQUIRE(ci_t::inst_cnt == 0); 75 | lf::deallocate(ptri); 76 | lf::deallocate(pvec); 77 | lf::deallocate(plis); 78 | lf::deallocate(pci); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /test/unit_test/test.hpp: -------------------------------------------------------------------------------- 1 | #define CATCH_CONFIG_DISABLE_MATCHERS 2 | #include "catch.hpp" 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #define REQUIRE_SAME_T(...) static_assert( \ 13 | std::is_same_v<__VA_ARGS__>, \ 14 | #__VA_ARGS__ " are not the same.") 15 | 16 | namespace { 17 | 18 | template 19 | struct counted { 20 | template 21 | counted(T c) noexcept: 22 | valid(true), cnt(c) { 23 | ++inst_cnt; 24 | } 25 | 26 | ~counted() { 27 | if (valid) --inst_cnt; 28 | } 29 | 30 | counted(const counted& ci) noexcept: 31 | valid(ci.valid), cnt(ci.cnt) { 32 | if (valid) ++inst_cnt; 33 | } 34 | 35 | counted(counted&& ci) noexcept: 36 | valid(ci.valid), cnt(ci.cnt) { 37 | ci.valid = false; 38 | } 39 | 40 | counted& operator=(counted ci) noexcept { 41 | swap(*this, ci); 42 | return *this; 43 | } 44 | 45 | friend void swap(counted& a, counted& b) noexcept { 46 | std::swap(a.valid, b.valid); 47 | std::swap(a.cnt, b.cnt); 48 | } 49 | 50 | void require(Cnt cnt, int inst_cnt) const { 51 | REQUIRE(this->cnt == cnt); 52 | REQUIRE(counted::inst_cnt == inst_cnt); 53 | } 54 | 55 | bool valid; 56 | Cnt cnt; 57 | 58 | inline static int inst_cnt = 0; 59 | }; 60 | 61 | } // unnamed namespace 62 | 63 | struct triple { 64 | int a, b, c; 65 | }; 66 | 67 | template 68 | void for_each(F&&) noexcept {} 69 | 70 | template 71 | void for_each(F&& f, T&& v, Us&&... us) { 72 | std::invoke(std::forward(f), std::forward(v)); 73 | for_each(std::forward(f), std::forward(us)...); 74 | } 75 | 76 | template 77 | void memset(T& v, int fill = -1) noexcept { 78 | std::memset((void*)&v, fill, sizeof(T)); 79 | } 80 | 81 | template 82 | T* alloc(int fill = -1) { 83 | auto p = (T*)operator new(sizeof(T)); 84 | memset(*p, fill); 85 | return p; 86 | } 87 | 88 | template 89 | bool memcmp(const T& a, const T& b) noexcept { 90 | return std::memcmp(&a, &b, sizeof(a)) == 0; 91 | } 92 | -------------------------------------------------------------------------------- /test/unit_test/allocator.cpp: -------------------------------------------------------------------------------- 1 | #include "../../lf/allocator.hpp" 2 | #include "../../lf/allocator.hpp" 3 | 4 | #include "test.hpp" 5 | 6 | namespace { 7 | 8 | void require_capacity_2(lf::allocator& allo) { 9 | auto p1 = allo.try_allocate(); 10 | REQUIRE(p1 == 0); 11 | auto p2 = allo.try_allocate(); 12 | REQUIRE(p2 == 1); 13 | REQUIRE(allo.try_allocate() == lf::null); 14 | allo.deallocate(p1); 15 | allo.deallocate(p2); 16 | REQUIRE(allo.try_allocate() == 1); 17 | REQUIRE(allo.try_allocate() == 0); 18 | REQUIRE(allo.try_allocate() == lf::null); 19 | allo.deallocate(p2); 20 | allo.deallocate(p1); 21 | } 22 | 23 | } // unnamed namespace 24 | 25 | TEST_CASE("allocator") { 26 | SECTION("ctor") { 27 | lf::allocator a1, a2(0), a3(2); 28 | REQUIRE(a1.try_allocate() == lf::null); 29 | REQUIRE(a2.try_allocate() == lf::null); 30 | require_capacity_2(a3); 31 | } 32 | SECTION("reset") { 33 | lf::allocator a; 34 | REQUIRE(a.try_allocate() == lf::null); 35 | a.reset(2); 36 | require_capacity_2(a); 37 | a.reset(0); 38 | REQUIRE(a.try_allocate() == lf::null); 39 | } 40 | SECTION("reset 2") { 41 | auto v = 0; 42 | auto inc_fn = [](auto& v) noexcept { ++v; }; 43 | lf::allocator a; 44 | REQUIRE(a.try_allocate() == lf::null); 45 | a.reset(2, inc_fn, v); 46 | REQUIRE(v == 1); 47 | require_capacity_2(a); 48 | a.reset(0, inc_fn, v); 49 | REQUIRE(v == 2); 50 | REQUIRE(a.try_allocate() == lf::null); 51 | } 52 | SECTION("deref/del") { 53 | using ci_t = counted; 54 | lf::allocator allo(2); 55 | auto p1 = allo.try_allocate(); 56 | auto p2 = allo.try_allocate(); 57 | REQUIRE(allo.try_allocate() == lf::null); 58 | auto& nod1 = allo.deref(p1); 59 | auto& nod2 = allo.deref(p2); 60 | auto ptrdiff = &nod2 - &nod1; 61 | REQUIRE(ptrdiff == 1); 62 | lf::init(&nod1.val, 1); 63 | lf::init(&nod2.val, 2); 64 | REQUIRE(ci_t::inst_cnt == 2); 65 | allo.del(p1); 66 | allo.del(p2); 67 | REQUIRE(ci_t::inst_cnt == 0); 68 | REQUIRE(allo.try_allocate() == p2); 69 | REQUIRE(allo.try_allocate() == p1); 70 | REQUIRE(allo.try_allocate() == lf::null); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /lf/allocator.hpp: -------------------------------------------------------------------------------- 1 | #ifndef LF_ALLOCATOR_HPP 2 | #define LF_ALLOCATOR_HPP 3 | 4 | #include "memory.hpp" 5 | #include "utility.hpp" 6 | 7 | #include 8 | 9 | #include "prolog.inc" 10 | 11 | template 12 | class allocator { 13 | public: 14 | struct node { 15 | T val; 16 | std::atomic_uint32_t next; 17 | }; 18 | 19 | allocator() noexcept = default; 20 | 21 | explicit allocator(std::uint32_t capacity): 22 | backup(allocate(capacity)), 23 | head(cp_t{capacity ? 0 : null}) { 24 | link(capacity); 25 | } 26 | 27 | ~allocator() { 28 | lf::deallocate(backup); 29 | } 30 | 31 | allocator(const allocator&) = delete; 32 | allocator& operator=(const allocator&) = delete; 33 | 34 | void reset(std::uint32_t capacity) { 35 | lf::deallocate(std::exchange(backup, allocate(capacity))); 36 | head.store({capacity ? 0 : null}, rlx); 37 | link(capacity); 38 | } 39 | 40 | template 41 | void reset(std::uint32_t capacity, F&& f, Args&&... args) { 42 | auto newbackup = allocate(capacity); 43 | std::invoke(std::forward(f), std::forward(args)...); 44 | lf::deallocate(std::exchange(backup, newbackup)); 45 | head.store({capacity ? 0 : null}, rlx); 46 | link(capacity); 47 | } 48 | 49 | std::uint32_t try_allocate() noexcept { 50 | cp_t newhd, hd(head.load(acq)); 51 | do { 52 | if (hd.ptr == null) return null; 53 | auto& nod = deref(hd.ptr); 54 | newhd.ptr = nod.next.load(rlx); 55 | newhd.cnt = hd.cnt + 1; 56 | } 57 | while (!head.compare_exchange_weak(hd, newhd, rlx, acq)); 58 | return hd.ptr; 59 | } 60 | 61 | void deallocate(std::uint32_t p) noexcept { 62 | auto& nod = deref(p); 63 | cp_t newhd{p}, hd(head.load(rlx)); 64 | do { 65 | nod.next.store(hd.ptr, rlx); 66 | newhd.cnt = hd.cnt; 67 | } 68 | while (!head.compare_exchange_weak(hd, newhd, rel, rlx)); 69 | } 70 | 71 | node& deref(std::uint32_t ptr) noexcept { 72 | return backup[ptr]; 73 | } 74 | 75 | void del(std::uint32_t p) noexcept { 76 | auto& nod = deref(p); 77 | uninit(&nod.val); 78 | deallocate(p); 79 | } 80 | 81 | private: 82 | void link(std::uint32_t capacity) noexcept { 83 | if (!capacity) return; 84 | std::uint32_t i = 0, ni = 1; 85 | while (ni < capacity) { 86 | init(&backup[i].next, ni); 87 | i = ni++; 88 | } 89 | init(&backup[i].next, null); 90 | } 91 | 92 | node* backup{}; 93 | std::atomic head{cp_t{}}; 94 | }; 95 | 96 | #include "epilog.inc" 97 | 98 | #endif // LF_ALLOCATOR_HPP 99 | -------------------------------------------------------------------------------- /lf/queue.hpp: -------------------------------------------------------------------------------- 1 | #ifndef LF_QUEUE_HPP 2 | #define LF_QUEUE_HPP 3 | 4 | #include "common.hpp" 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | namespace lf { 11 | 12 | namespace queue_impl { 13 | 14 | template 15 | struct node { 16 | ~node() { 17 | // delete nullptr is OK 18 | delete data.load(rlx); 19 | } 20 | 21 | std::atomic data{}; 22 | std::atomic next{}; 23 | std::atomic_uint64_t cnt{2}; 24 | }; 25 | 26 | } // namespace queue_impl 27 | 28 | template 29 | class queue { 30 | using node = queue_impl::node; 31 | using counted_ptr = lf::counted_ptr; 32 | 33 | public: 34 | // copy control 35 | queue(const queue&) = delete; 36 | queue& operator=(const queue&) = delete; 37 | 38 | ~queue() { 39 | auto p = head.load(rlx).p; 40 | while (p) { 41 | delete std::exchange(p, p->next.load(rlx)); 42 | } 43 | } 44 | 45 | // construct 46 | queue(): 47 | head({new node{}}), 48 | tail(head.load(rlx)) { 49 | } 50 | 51 | // modifier 52 | bool try_pop(T& v) noexcept { 53 | auto oldhead = head.load(rlx); 54 | while (true) { 55 | hold_ptr(head, oldhead, acq, rlx); 56 | auto p = oldhead.p; 57 | if (p == tail.load(acq).p) { 58 | unhold_ptr_acq(p); 59 | return false; 60 | } 61 | counted_ptr newhead{p->next.load(rlx)}; 62 | if (head.compare_exchange_strong(oldhead, newhead, rel, rlx)) { 63 | v = std::move(*p->data.load(rlx)); 64 | unhold_ptr_rel(oldhead, 1); 65 | return true; 66 | } 67 | unhold_ptr_acq(p); 68 | } 69 | } 70 | 71 | template 72 | void emplace(Us&&... args) { 73 | auto dat = std::make_unique(std::forward(args)...); 74 | auto nod = std::make_unique(); 75 | auto oritail = tail.load(rlx); 76 | do { 77 | if (!nod) nod = std::make_unique(); 78 | hold_ptr(tail, oritail, rlx, rlx); 79 | auto p = oritail.p; 80 | T* oridat = nullptr; 81 | if (p->data.compare_exchange_strong(oridat, dat.get(), rlx, rlx)) { 82 | dat.release(); 83 | } 84 | node *orinod = nullptr, *neotailnod = nod.get(); 85 | if (p->next.compare_exchange_strong(orinod, neotailnod, rlx, rlx)) { 86 | nod.release(); 87 | } 88 | else { 89 | neotailnod = orinod; 90 | } 91 | counted_ptr neotail{neotailnod}; 92 | if (tail.compare_exchange_strong(oritail, neotail, rel, rlx)) { 93 | unhold_ptr_acq(oritail); 94 | oritail = neotail; 95 | } 96 | else { 97 | unhold_ptr_acq(p); 98 | } 99 | } 100 | while (dat); 101 | } 102 | 103 | private: 104 | std::atomic head; 105 | std::atomic tail; 106 | }; 107 | 108 | } // namespace lf 109 | 110 | #endif // LF_QUEUE_HPP 111 | -------------------------------------------------------------------------------- /test/perf_test/simulator2.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SIMULATOR2_HPP 2 | #define SIMULATOR2_HPP 3 | 4 | #include "utility.hpp" 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | class simulator2 { 15 | public: 16 | using fn_t = void(*)(); 17 | 18 | static void configure( 19 | std::uint8_t thread_cnt, 20 | std::chrono::minutes expected_duration, 21 | std::vector fn) { 22 | simulator2::thread_cnt = thread_cnt; 23 | simulator2::expected_duration = expected_duration; 24 | sync.expected_cnt = thread_cnt; 25 | op_cnt = fn.size(); 26 | simulator2::fn = std::move(fn); 27 | per_thread.resize(thread_cnt); 28 | } 29 | 30 | static void kickoff() { 31 | thread.reserve(thread_cnt); 32 | for (std::uint8_t i = 0; i < thread_cnt; ++i) { 33 | thread.emplace_back(&per_thread_t::sync_and_run, &per_thread[i]); 34 | } 35 | std::this_thread::sleep_for(expected_duration); 36 | stop.store(true, std::memory_order_release); 37 | for_each_element(&std::thread::join, thread.begin(), thread.end()); 38 | } 39 | 40 | static void print_results() { 41 | auto [cnt, di, da] = per_thread[0].results(); 42 | for (std::uint8_t i = 1; i < thread_cnt; ++i) { 43 | auto [cur_cnt, cur_di, cur_da] = per_thread[i].results(); 44 | cnt += cur_cnt; 45 | di = std::min(di, cur_di); 46 | da = std::max(da, cur_da); 47 | } 48 | auto dur = (da - di).count(); 49 | std::cout << "count: " << cnt << '\n' 50 | << "ticks: " << dur << '\n' 51 | << "ratio: " << cnt / dur << std::endl; 52 | } 53 | 54 | private: 55 | class per_thread_t { 56 | public: 57 | per_thread_t() { 58 | for (std::uint8_t i = 0; i < op_cnt; ++i) { 59 | op_seq.insert(op_seq.end(), 1_K, i); 60 | } 61 | std::shuffle(op_seq.begin(), op_seq.end(), rnd); 62 | } 63 | 64 | void sync_and_run() { 65 | sync(); 66 | di = tick::now(); 67 | run(); 68 | da = tick::now(); 69 | } 70 | 71 | std::tuple 72 | results() const noexcept { 73 | return {cnt, di, da}; 74 | } 75 | 76 | private: 77 | void run() { 78 | while (!stop.load(std::memory_order_acquire)) { 79 | auto op = op_seq[++cnt % op_seq.size()]; 80 | fn[op](); 81 | } 82 | } 83 | 84 | std::vector op_seq; 85 | 86 | std::uint64_t cnt{}; 87 | tick::time_point di, da; 88 | }; 89 | 90 | inline static std::uint8_t thread_cnt; 91 | inline static std::chrono::minutes expected_duration; 92 | inline static sync_point sync; 93 | inline static std::uint8_t op_cnt; 94 | inline static std::vector fn; 95 | inline static std::vector per_thread; 96 | 97 | inline static std::vector thread; 98 | 99 | inline static std::atomic_bool stop{false}; 100 | inline static std::mt19937 rnd{}; 101 | }; 102 | 103 | #endif // SIMULATOR2_HPP 104 | -------------------------------------------------------------------------------- /test/perf_test/utility.hpp: -------------------------------------------------------------------------------- 1 | #ifndef UTILITY_HPP 2 | #define UTILITY_HPP 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | #define ERROR(...) throw ::std::runtime_error(::mkstr(__VA_ARGS__)) 20 | #define CONFIRM_MEMORY_FOOTPRINT(...) ::impl::confirm_memory_footprint(#__VA_ARGS__, __VA_ARGS__) 21 | 22 | using tick = std::chrono::high_resolution_clock; 23 | 24 | template 25 | using matrix = std::vector>; 26 | 27 | class sync_point { 28 | public: 29 | std::uint32_t expected_cnt; 30 | 31 | sync_point(std::uint32_t expected_cnt = 0) noexcept: 32 | expected_cnt(expected_cnt) { 33 | // nop 34 | } 35 | 36 | void operator()() noexcept { 37 | if (wait_cnt.fetch_add(1, std::memory_order_release) + 1 == expected_cnt) { 38 | (void)wait_cnt.load(std::memory_order_acquire); 39 | wait_cnt.store(0, std::memory_order_release); 40 | } 41 | else { 42 | while (wait_cnt.load(std::memory_order_acquire) != 0); 43 | } 44 | } 45 | 46 | private: 47 | std::atomic_uint32_t wait_cnt{0}; 48 | }; 49 | 50 | inline constexpr auto operator""_K(unsigned long long v) noexcept { return v * 1000; } 51 | inline constexpr auto operator""_M(unsigned long long v) noexcept { return v * 1000'000; } 52 | inline constexpr auto operator""_B(unsigned long long v) noexcept { return v * 1000'000'000; } 53 | 54 | template 55 | void for_each_element(F&& f, InputIt first, InputIt last) { 56 | while (first != last) { 57 | std::invoke(std::forward(f), *first++); 58 | } 59 | } 60 | 61 | template 62 | std::string mkstr(const Us&... vs) { 63 | std::ostringstream os; 64 | (void)(os << ... << vs); 65 | return os.str(); 66 | } 67 | 68 | inline void validate_thread_cnt(std::size_t thread_cnt) { 69 | if (thread_cnt == 0) ERROR("thread_cnt == 0"); 70 | if (thread_cnt > std::thread::hardware_concurrency()) { 71 | ERROR("thread_cnt > hardware_concurrency (", 72 | std::thread::hardware_concurrency(), ')'); 73 | } 74 | } 75 | 76 | inline void prompt() { 77 | std::cout << "Continue? [y/n] " << std::flush; 78 | std::string str; 79 | std::getline(std::cin, str); 80 | if (str != "y") ERROR("User cancel."); 81 | } 82 | 83 | namespace impl { 84 | 85 | inline 86 | void print_memory_footprint(std::size_t tot, const char*) { 87 | std::cout << " -------------\n" 88 | << " total: " << tot / 1_M << "M\n"; 89 | } 90 | 91 | template 92 | void print_memory_footprint(std::size_t tot, const char* p, std::size_t sz, Args... args) { 93 | std::cout << " " << p << ": " << sz / 1_M << "M\n"; 94 | print_memory_footprint(tot + sz, std::strchr(p, '\0') + 1, args...); 95 | } 96 | 97 | template 98 | void confirm_memory_footprint(const char* str, Args... args) { 99 | std::string names; 100 | while (auto c = *str++) { 101 | if (std::isspace(c)) continue; 102 | names.push_back(c == ',' ? '\0' : c); 103 | } 104 | std::cout << "Estimated memory footprint\n"; 105 | print_memory_footprint(0, names.c_str(), args...); 106 | prompt(); 107 | } 108 | 109 | } // namespace impl 110 | 111 | #endif // UTILITY_HPP 112 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A C++17 Lock-Free Data Structure Library 2 | 3 | [![Build Status](https://travis-ci.org/Lingxi-Li/lock_free.svg?branch=master)](https://travis-ci.org/Lingxi-Li/lock_free) 4 | [![codecov](https://codecov.io/gh/Lingxi-Li/lock_free/branch/master/graph/badge.svg)](https://codecov.io/gh/Lingxi-Li/lock_free) 5 | 6 | **Documentation is currently outdated and invalid.** 7 | 8 | This is a library for lock-free data structures with the following features. 9 | 10 | - Written purely in standard C++17 with no platform-dependent code. 11 | - Header-only. 12 | - Designed for low worst-case latency. 13 | - Designed for exception safety. 14 | - Provides first-class support, like emplace construction, for aggregate types. 15 | 16 | ### Contents 17 | 18 | - [Requirements](#requirements) 19 | - [Repository Structure](#repository-structure) 20 | - [Build](#build) 21 | - [Unit Test](#unit-test) 22 | - [Reference Doc](#reference-doc) 23 | 24 | ### Requirements 25 | 26 | - A decent C++17 implementation. Tested: 27 | - MacPorts clang 6.0.0 28 | - MacPorts gcc 7.3.0 29 | - 64-bit pointer to avoid [padding atomic][4]. 30 | - [Lock-free 128-bit atomic](#build). 31 | 32 | ### Repository Structure 33 | 34 | ~~~ 35 | doc.............Reference doc 36 | lf..............Library headers 37 | test 38 | unit_test 39 | ~~~ 40 | 41 | ### Build 42 | 43 | The C++ standard does not require atomics, other than [`std::atomic_flag`][8], to be lock-free. 44 | Lock-free 128-bit atomic, in particular, demands both CPU and C++ implementation support, 45 | plus proper build setup. 46 | 47 | - All modern x86-64 CPUs provide support[[r][1]]. 48 | - C++ implementation supportability 49 | - MacPorts clang 6.0.0 is verified to provide support. 50 | - Supportability is unclear for MacPorts gcc 7.3.0. 51 | - It is [said][1] that MSVC does not provide support. 52 | - For clang/gcc, compile with [`-mcx16`][3]. 53 | 54 | [Here][2] are some notes for verifying a genuine lock-free build. 55 | That said, [`is_lock_free()`][5] test is unreliable in practice. 56 | 57 | ### Unit Test 58 | 59 | Build all source files in `test/unit_test` to get the unit test executable. 60 | For example, using MacPorts clang 6.0.0, the following command line 61 | builds the test executable `bin/unit_test`. 62 | 63 | ~~~ 64 | clang++-mp-6.0 test/unit_test/*.cpp -o bin/unit_test -std=c++17 -mcx16 65 | ~~~ 66 | 67 | Note that gcc may additionally need `-latomic`. 68 | 69 | The test executable uses [Catch2][6] and supports various [command line arguments][7]. 70 | There is a dedicated test case named `foo bar` for each header `lf/foo/bar.hpp`. 71 | Executing with no argument runs all test cases. 72 | 73 | ### Reference Doc 74 | 75 | The library is still under early development. 76 | Reference for finished components can be found [here](doc/readme.md#reference). 77 | 78 | Planned lock-free structures: 79 | 80 | - [X] Stack 81 | - [ ] Queue 82 | - [ ] Atomic shared pointer 83 | - [ ] Thread pool 84 | 85 | - [ ] Fixed-capacity allocator 86 | - [ ] Fixed-capacity stack 87 | - [ ] Fixed-capacity queue 88 | - [ ] Fixed-capacity thread pool 89 | 90 | [1]:https://stackoverflow.com/a/38991835/1348273 91 | [2]:https://stackoverflow.com/q/49848793/1348273 92 | [3]:https://gcc.gnu.org/onlinedocs/gcc-7.3.0/gcc/x86-Options.html#x86-Options 93 | [4]:https://stackoverflow.com/q/48947428/1348273 94 | [5]:http://en.cppreference.com/w/cpp/atomic/atomic/is_lock_free 95 | [6]:https://github.com/catchorg/Catch2/blob/master/README.md#top 96 | [7]:https://github.com/catchorg/Catch2/blob/master/docs/command-line.md#top 97 | [8]:http://en.cppreference.com/w/cpp/atomic/atomic_flag 98 | -------------------------------------------------------------------------------- /test/perf_test/cli.hpp: -------------------------------------------------------------------------------- 1 | #ifndef CLI_HPP 2 | #define CLI_HPP 3 | 4 | #include "utility.hpp" 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | #define MAIN(...) \ 18 | void main_(__VA_ARGS__); \ 19 | int main(int argc, char* argv[]) { \ 20 | return impl::guarded_run(main_, argc, (const char**)argv, #__VA_ARGS__); \ 21 | } \ 22 | void main_(__VA_ARGS__) 23 | 24 | template 25 | struct optional { 26 | T value{def}; 27 | operator T&() noexcept { return value; } 28 | operator const T&() const noexcept { return value; } 29 | }; 30 | 31 | namespace impl { 32 | 33 | template 34 | void set_arg(T& arg, const char* str) { 35 | std::istringstream is(str); 36 | if (!(is >> arg) || is.peek() != std::char_traits::eof()) { 37 | throw std::invalid_argument( 38 | mkstr("Invalid argument: ", str)); 39 | } 40 | } 41 | 42 | inline void set_arg(std::string& arg, const char* str) { 43 | arg = str; 44 | } 45 | 46 | inline void set_args(const char** p, const char** end) { 47 | if (p != end) { 48 | throw std::invalid_argument("Too many arguments."); 49 | } 50 | } 51 | 52 | template 53 | void set_args(const char** p, const char** end, optional& arg, Us&... args); 54 | 55 | template 56 | void set_args(const char** p, const char** end, T& arg, Us&... args) { 57 | if (p == end) throw std::invalid_argument("Too few arguments."); 58 | set_arg(arg, *p++); 59 | set_args(p, end, args...); 60 | } 61 | 62 | template 63 | void set_args(const char** p, const char** end, optional& arg, Us&... args) { 64 | if (p == end) return; 65 | set_arg(arg, *p++); 66 | set_args(p, end, args...); 67 | } 68 | 69 | template 70 | std::string format(std::chrono::duration du) { 71 | using namespace std::chrono; 72 | using days = duration>; 73 | auto d = duration_cast(du); 74 | auto h = duration_cast(du -= d); 75 | auto m = duration_cast(du -= h); 76 | auto s = duration_cast(du -= m); 77 | char hms[17] = "HH:mm:ss"; 78 | std::sprintf(hms, "%02d:%02d:%02d", 79 | (int)h.count(), (int)m.count(), (int)s.count()); 80 | return mkstr(d.count(), "d ", hms); 81 | } 82 | 83 | template 84 | int guarded_run( 85 | void(*f)(Us...), int argc, const char** argv, const char* params) noexcept { 86 | auto res = 0; 87 | auto epoch = tick::now(); 88 | try { 89 | auto p = argv + 1, end = argv + argc; 90 | std::tuple args; 91 | std::apply([p, end](auto&... args) { 92 | set_args(p, end, args...); 93 | }, args); 94 | std::apply(f, std::move(args)); 95 | } 96 | catch (const std::invalid_argument& e) { 97 | std::cout << e.what() << '\n' 98 | << "Usage: (" << params << ')' << std::endl; 99 | return EXIT_FAILURE; 100 | } 101 | catch (const std::exception& e) { 102 | std::cout << "std::exception caught with message:\n" 103 | << e.what() << std::endl; 104 | res = EXIT_FAILURE; 105 | } 106 | catch (...) { 107 | std::cout << "Unknown exception caught." << std::endl; 108 | res = EXIT_FAILURE; 109 | } 110 | std::cout << "-------------------\n" 111 | << "Ran for " << format(tick::now() - epoch) << std::endl; 112 | return res; 113 | } 114 | 115 | } // namespace impl 116 | 117 | #endif // CLI_HPP 118 | -------------------------------------------------------------------------------- /doc/lf/allocator.md: -------------------------------------------------------------------------------- 1 | ## Header `lf/allocator.hpp` 2 | 3 | This header provides a fixed-capacity allocator. 4 | It implements a kind of memory pool that preallocates required memory resources from OS. 5 | Allocations and deallocations from the allocator are then lock-free. 6 | 7 | - [Synopsis](#synopsis) 8 | - [Details](#details) 9 | 10 | ### Synopsis 11 | 12 | ~~~C++ 13 | template 14 | class allocator { 15 | public: 16 | struct node { 17 | T val; 18 | std::atomic next; 19 | }; 20 | 21 | allocator() noexcept = default; 22 | explicit allocator(std::size_t capacity); 23 | ~allocator(); 24 | 25 | allocator(const allocator&) = delete; 26 | allocator& operator=(const allocator&) = delete; 27 | 28 | void reset(std::size_t capacity); 29 | template 30 | void reset(std::size_t capacity, F&& f, Args&&... args); 31 | 32 | node* try_allocate() noexcept; 33 | void deallocate(node* p) noexcept; 34 | 35 | node* try_make(T&& v) noexcept; 36 | void del(node* p) noexcept; 37 | }; 38 | ~~~ 39 | 40 | ### Details 41 | 42 | ~~~C++ 43 | struct node { 44 | T val; 45 | std::atomic next; 46 | }; 47 | ~~~ 48 | 49 | Specifies the node structure that is allocated/deallocated by the allocator. 50 | 51 | `val` is the user payload. 52 | The allocator does not manage its initialization/uninitialization. 53 | Specifically, it is not initialized as part of node allocation, 54 | and should be uninitialized by user before node deallocation. 55 | 56 | `next` is used internally by the allocator, and is exposed for reuse opportunity by user. 57 | Its initialization/uninitialization is managed by the allocator. 58 | Specifically, it is initialized in an allocated node, 59 | and does not need uninitialization before deallocating the node. 60 | 61 | -------------------------------------------------------------------------------- 62 | 63 | ~~~C++ 64 | allocator() noexcept = default; 65 | ~~~ 66 | 67 | Initializes a zero-capacity allocator. 68 | 69 | -------------------------------------------------------------------------------- 70 | 71 | ~~~C++ 72 | explicit allocator(std::size_t capacity); 73 | ~~~ 74 | 75 | Initializes an allocator of a specified capacity. 76 | For non-zero capacity, preallocates required memory resources from OS. 77 | 78 | -------------------------------------------------------------------------------- 79 | 80 | ~~~C++ 81 | ~allocator(); 82 | ~~~ 83 | 84 | Deallocates allocated nodes, and returns preallocated memory resources to OS, if any. 85 | It is unnecessary to deallocate allocated nodes before the destructor call. 86 | However, user is still required to first uninitialize `node::val`. 87 | 88 | -------------------------------------------------------------------------------- 89 | 90 | ~~~C++ 91 | void reset(std::size_t capacity); 92 | 93 | template 94 | void reset(std::size_t capacity, F&& f, Args&&... args); 95 | ~~~ 96 | 97 | Resets allocator capacity. 98 | 99 | The first overload is semantically equivalent to uninitializing the current allocator, 100 | and initializing a new one with the specified capacity. 101 | Reseting to zero capacity returns preallocated memory resources to OS, if any. 102 | `node::val` in allocated nodes should be uninitialized before the call. 103 | The method itself provides strong exception safety. 104 | However, the requirement of first uninitializing `node::val` may pose an issue. 105 | For example, one may write the code 106 | 107 | ~~~C++ 108 | uninit_val(...); 109 | alloc.reset(...); 110 | ~~~ 111 | 112 | If the `reset()` call threw, `uninit_val()` would have already been called. 113 | The second overload is introduced to solve this issue. 114 | What it does is similar to 115 | 116 | ~~~C++ 117 | std::invoke(std::forward(f), std::forward(args)...); 118 | reset(capacity); 119 | ~~~ 120 | 121 | The invocation is intended to perform `node::val` uninitialization, and assumed to not throw. 122 | The method then provides strong exception safety. 123 | That is, if it threw, the invocation would not have happened. 124 | 125 | Both overloads are non-thread-safe. 126 | 127 | -------------------------------------------------------------------------------- 128 | 129 | ~~~C++ 130 | node* try_allocate() noexcept; 131 | ~~~ 132 | 133 | Tries to allocate a node. 134 | Returns `nullptr` on failure. 135 | May fail spuriously. 136 | 137 | -------------------------------------------------------------------------------- 138 | 139 | ~~~C++ 140 | void deallocate(node* p) noexcept; 141 | ~~~ 142 | 143 | Returns node `p` to the allocator. 144 | `p->val` should be uninitialized first. 145 | 146 | -------------------------------------------------------------------------------- 147 | 148 | ~~~C++ 149 | node* try_make(T&& v) noexcept; 150 | ~~~ 151 | 152 | Tries to allocate a node and move-construct `node::val` with `v`. 153 | Returns `nullptr` on failure with `v` intact. 154 | May fail spuriously. 155 | 156 | Requires `T&& v` in order to provide no-throw guarantee. 157 | To copy-construct `node::val`, use code like 158 | 159 | ~~~C++ 160 | auto vv = v; 161 | auto p = alloc.try_make(std::move(vv)); 162 | ~~~ 163 | 164 | -------------------------------------------------------------------------------- 165 | 166 | ~~~C++ 167 | void del(node* p) noexcept; 168 | ~~~ 169 | 170 | Uninitializes `p->val` and returns node `p` to the allocator. 171 | -------------------------------------------------------------------------------- /doc/lf/split_ref.md: -------------------------------------------------------------------------------- 1 | ## Header `lf/split_ref.hpp` 2 | 3 | This header provides utilities for implementing the split reference counts scheme. 4 | 5 | - [Split Reference Counts](#split-reference-counts) 6 | - [Encoding](#encoding) 7 | - [Synopsis](#synopsis) 8 | - [Details](#details) 9 | 10 | ### Split Reference Counts 11 | 12 | This is an extension of the [ordinary reference counting scheme][3]. 13 | It distinguishes and counts external and internal references. 14 | External references are transient. 15 | Internal references are ordinary persistent references. 16 | 17 | #### Encoding 18 | 19 | Use a single 64-bit unsigned integer to encode both external and internal count. 20 | The higher/lower 32-bits encode external/internal count, respectively. 21 | 22 | *Notes:* Use unsigned type to [wrap around rather than overflowing][1]. 23 | Use higher bits for external count to allow external count wrap around without 24 | interfering with internal count. 25 | External count wrap around is OK, but not internal count. 26 | 27 | ### Synopsis 28 | 29 | ~~~C++ 30 | // One external count. 31 | inline constexpr auto ext_cnt = (std::uint64_t)1 << 32; 32 | 33 | // Counted pointer structure. 34 | template 35 | struct counted_ptr { 36 | T* ptr{}; 37 | std::uint64_t cnt{}; 38 | }; 39 | 40 | // Atomic counted pointer. 41 | template 42 | using atomic_counted_ptr = std::atomic>; 43 | 44 | // Holds the pointer with an external reference. 45 | template 46 | void hold_ptr( 47 | atomic_counted_ptr& stub, 48 | counted_ptr& ori, 49 | std::memory_order mem_ord) noexcept; 50 | 51 | // Holds the pointer with an external reference if it is not null. 52 | template 53 | bool hold_ptr_if_not_null( 54 | atomic_counted_ptr& stub, 55 | counted_ptr& ori, 56 | std::memory_order mem_ord) noexcept; 57 | 58 | // Unholds the pointer by releasing an external reference. 59 | template 60 | void unhold_ptr_acq(T* p, Del&& del = Del{}) noexcept; 61 | 62 | // Unholds the pointer by committing/releasing multiple references. 63 | template 64 | void unhold_ptr_acq( 65 | counted_ptr cp, 66 | std::uint64_t int_cnt, 67 | Del&& del = Del{}) noexcept; 68 | 69 | // Unholds the pointer by committing/releasing multiple references. 70 | template 71 | void unhold_ptr_rel( 72 | counted_ptr cp, 73 | std::uint64_t int_cnt, 74 | Del&& del = Del{}) noexcept; 75 | ~~~ 76 | 77 | ### Details 78 | 79 | ~~~C++ 80 | template 81 | struct counted_ptr { 82 | T* ptr{}; 83 | std::uint64_t cnt{}; 84 | }; 85 | 86 | template 87 | using atomic_counted_ptr = std::atomic>; 88 | ~~~ 89 | 90 | There are many uses of `cnt`. 91 | In the [split reference counts scheme](#split-reference-counts), 92 | it is the uncommitted external count. 93 | Before dereferencing `ptr`, hold the pointer with an external reference 94 | by increasing `cnt` by `ext_cnt`. 95 | 96 | `atomic_counted_ptr` has a trivial default constructor that does nothing[[r][2]]. 97 | This effectively ignores `counted_ptr`'s default member initializers[[r][6]]. 98 | 99 | `atomic_counted_ptr` requires 64-bit pointers to work, which make 100 | `counted_ptr` 128-bit with no padding. This avoids the [atomic padding issue][4]. 101 | 102 | -------------------------------------------------------------------------------- 103 | 104 | ~~~C++ 105 | template 106 | void hold_ptr( 107 | atomic_counted_ptr& stub, 108 | counted_ptr& ori, 109 | std::memory_order mem_ord) noexcept; 110 | 111 | template 112 | bool hold_ptr_if_not_null( 113 | atomic_counted_ptr& stub, 114 | counted_ptr& ori, 115 | std::memory_order mem_ord) noexcept; 116 | ~~~ 117 | 118 | Holds `stub` with an external reference. 119 | `ori` is passed an initial guess of `stub`. 120 | The resulting value is stored back to `ori`. 121 | `mem_ord` specifies the memory order semantics of the read-modify-write operation on `stub`. 122 | 123 | `hold_ptr_if_not_null()` fails if `stub.ptr` is found to be null. 124 | In this case, it returns false and does not modify `stub`, ignoring `mem_ord`. 125 | If `ori.ptr` is passed null, fails immediately. 126 | 127 | -------------------------------------------------------------------------------- 128 | 129 | ~~~C++ 130 | template 131 | void unhold_ptr_acq(T* p, Del&& del = Del{}) noexcept; 132 | ~~~ 133 | 134 | Unholds `p` by releasing one external reference. 135 | If no reference then remains, deletes `p` using the supplied deleter `del`, 136 | and `unhold_ptr_rel()`, if any, happens-before the deletion. 137 | `T` is required to have member `std::atomic_uint64_t cnt` 138 | with the split reference counts [encoding](#encoding). 139 | 140 | -------------------------------------------------------------------------------- 141 | 142 | ~~~C++ 143 | template 144 | void unhold_ptr_acq( 145 | counted_ptr cp, 146 | std::uint64_t int_cnt, 147 | Del&& del = Del{}) noexcept; 148 | 149 | template 150 | void unhold_ptr_rel( 151 | counted_ptr cp, 152 | std::uint64_t int_cnt, 153 | Del&& del = Del{}) noexcept; 154 | ~~~ 155 | 156 | Unholds `p` by 157 | 158 | 1. releasing one external reference, 159 | 2. committing the external count `cp.cnt`, 160 | 3. releasing `int_cnt` internal references. 161 | 162 | If no reference then remains, deletes `cp.ptr` using the supplied deleter `del`. 163 | `unhold_ptr_rel()`, if any, happens-before the deletion performed by `unhold_ptr_acq()`. 164 | `T` is required to have member `std::atomic_uint64_t cnt` 165 | with the split reference counts [encoding](#encoding). 166 | 167 | [1]: http://en.cppreference.com/w/cpp/language/operator_arithmetic#Overflows 168 | [2]: http://en.cppreference.com/w/cpp/atomic/atomic/atomic 169 | [3]: https://en.wikipedia.org/wiki/Reference_counting 170 | [4]: https://stackoverflow.com/q/48947428/1348273 171 | [5]: https://superuser.com/a/941175/517080 172 | [6]: https://stackoverflow.com/q/49387069/1348273 173 | [7]: https://stackoverflow.com/q/49400942/1348273 174 | -------------------------------------------------------------------------------- /doc/lf/memory.md: -------------------------------------------------------------------------------- 1 | ## Header `lf/memory.hpp` 2 | 3 | This header provides utilities for memory management and object initialization. 4 | They are used exclusively in the library implementation. 5 | The built-in utilities are never used directly. 6 | 7 | - [Introspective Initialization](#introspective-initialization) 8 | - [Exception Safety](#exception-safety) 9 | - [Non-Template Template Wrapper](#non-template-template-wrapper) 10 | - [Why Not Built-In Utilities](#why-not-built-in-utilities) 11 | - [Synopsis](#synopsis) 12 | - [Details](#details) 13 | 14 | ### Introspective Initialization 15 | 16 | List initialization using the `{}` syntax is often termed uniform/universal initialization. 17 | It applies to both aggregate and non-aggregate types. 18 | However, consistently doing list initialization is problematic in semantics. 19 | Because of this, `auto p = std::make_unique>(2, 1)` makes `[1, 1]` not `[2, 1]`. 20 | To avoid surprises, `()` is used instead of `{}` internally. 21 | The following code therefore does not compile (as of C++17) 22 | 23 | ~~~C++ 24 | struct pr { 25 | int a, b; 26 | }; 27 | auto p = std::make_unique(1, 2); 28 | ~~~ 29 | 30 | It would be ideal if there is some kind of introspective initialization that 31 | 32 | 1. does `()` initialization if possible, and 33 | 2. falls back to `{}` initialization otherwise, e.g., for aggregate types. 34 | 35 | This header provides tools to perform such introspective initialization. 36 | 37 | ### Exception Safety 38 | 39 | Creating an object in dynamic memory involves two steps, memory allocation and object initialization. 40 | There are two things to note concerning exception safety in this process. 41 | 42 | The first thing to note is that if object initialization threw, memory should be deallocated. 43 | Failing to do so results in memory leak. The other thing to note, which is less well-known, is that 44 | initializers should be evaluated after memory allocation. Consider the code. 45 | 46 | ~~~C++ 47 | auto p = std::make_unique( U(std::move(v)) ); 48 | ~~~ 49 | 50 | Assume that the initialization process `T( U(std::move(v)) )` does not throw. 51 | Since the initializer expression `U(std::move(v))` is evaluated after memory allocation, 52 | in case memory allocation threw, `v` would have already been moved/modified. 53 | Evaluating initializers after memory allocation avoids this issue. 54 | 55 | The (non-placement) new expression does this[[r][5]]. 56 | The code can thus be revised as 57 | 58 | ~~~C++ 59 | std::unique_ptr p(new T(U(std::move(v)))); 60 | ~~~ 61 | 62 | Not just that, new expression also makes sure that memory is deallocated if 63 | initialization throws (the first point to note). 64 | But still, the new expression is no panacea. 65 | It does not do [introspective initialization](#introspective-initialization). 66 | 67 | This header provides initialization tools that perform introspective initialization 68 | and deallocate memory in case of exception. It also provides helper macros that 69 | combine memory allocation and object initialization, similar to the new expression but 70 | with introspective initialization. 71 | 72 | ### Non-Template Template Wrapper 73 | 74 | More often than not, template arguments to function template are auto-deduced. 75 | This is so neat that you may forget its template nature... until you need to pass it along. 76 | Suppose the following function template 77 | 78 | ~~~C++ 79 | template 80 | void dismiss(T* p) noexcept; 81 | ~~~ 82 | 83 | It is common practice to invoke the function template with `dismiss(p)`, and have `T` auto-deduced from `p`. 84 | However, this does not work for `std::invoke(dismiss, p)`. 85 | In this case, one may have to `std::invoke(dismiss<...>, p)`, which is annoying. 86 | 87 | This issue can be addressed with a non-template template wrapper. 88 | 89 | ~~~C++ 90 | inline constexpr 91 | struct dismiss_t { 92 | template 93 | void operator()(T* p) const noexcept; 94 | } 95 | dismiss; 96 | ~~~ 97 | 98 | You still invoke with `dismiss(p)` and have auto-deduction in effect. 99 | But you can also pass `dismiss` along like `std::invoke(dismiss, p)`. 100 | 101 | This header provides non-template template wrappers. 102 | 103 | ### Why Not Built-In Utilities 104 | 105 | 1. They are untyped and C-style, e.g., see [`operator new()`][2]. 106 | Templates are thus provided with auto-deduced type and size, 107 | which offer a concise syntax and are less error-prone. 108 | 2. Built-in utilities are [delicate][1] and easy to get wrong, leading to intricate bugs. 109 | An example would be [ignoring the return value of placement new][3]. 110 | Robust counterparts are provided that avoid the pitfalls. 111 | 3. The syntax of built-in utilities is somewhat exotic and eye-piercing. 112 | Counterparts are provided to offer a uniform conventional function call syntax. 113 | 4. Using custom allocators is rare in practice. 114 | Writing allocator-aware data structures may not be worth the trouble. 115 | Sticking to the standard allocator may be a hassle too, 116 | for you have to either drag a stateless allocator object along, 117 | or create a new one every time you need the functionality. 118 | Besides, the allocator interface is unnecessarily complex, considering 119 | the library's simple needs. 120 | Free functions are apt here. 121 | 122 | ### Synopsis 123 | 124 | ~~~C++ 125 | template 126 | T* allocate(std::size_t n = 1); 127 | 128 | template 129 | T* try_allocate(std::size_t n = 1) noexcept; 130 | 131 | inline constexpr 132 | struct deallocate_t { 133 | void operator()(void* p) const noexcept; 134 | } 135 | deallocate; 136 | 137 | inline constexpr 138 | struct init_no_catch_t { 139 | template 140 | void operator()(T*& p, Us&&... us) const; 141 | template 142 | void operator()(T* const & p, Us&&... us) const; 143 | } 144 | init_no_catch; 145 | 146 | inline constexpr 147 | struct init_t { 148 | template 149 | void operator()(P&& p, Us&&... us) const; 150 | } 151 | init; 152 | 153 | inline constexpr 154 | struct uninit_t { 155 | template 156 | void operator()(T* p) const noexcept; 157 | } 158 | uninit; 159 | 160 | template 161 | T emplace(Us&&... us); 162 | 163 | LF_MAKE(p, T, ...) 164 | 165 | // un-initialize and deallocate 166 | inline constexpr 167 | struct dismiss_t { 168 | template 169 | void operator()(T* p) const noexcept; 170 | } 171 | dismiss; 172 | 173 | template 174 | using unique_ptr = std::unique_ptr; 175 | 176 | template 177 | auto init_unique(T* p, Us&&... us); 178 | 179 | LF_MAKE_UNIQUE(p, T, ...) 180 | ~~~ 181 | 182 | ### Details 183 | 184 | ~~~C++ 185 | inline constexpr 186 | struct init_no_catch_t { 187 | template 188 | void operator()(T*& p, Us&&... us) const; 189 | template 190 | void operator()(T* const & p, Us&&... us) const; 191 | } 192 | init_no_catch; 193 | 194 | inline constexpr 195 | struct init_t { 196 | template 197 | void operator()(P&& p, Us&&... us) const; 198 | } 199 | init; 200 | ~~~ 201 | 202 | [Intro-initializes](#introspective-initialization) a `T` (value type of `P`) object at the address 203 | referred to by `p` with `us...`, and sets `p` with the [return value of placement new][3] if possible 204 | (i.e., when `p` is a non-const l-value). In case of initialization exception, the `_no_catch` version 205 | does nothing, while the normal version deallocates `p` before propagating the exception. 206 | 207 | Note that do not code `init(allocate(), ...)` (and similarly `init_no_catch(allocate(), ...)`). 208 | Since argument evaluation order is [unspecified][6], the code does not guarantee that 209 | the initializer expression is evaluated after memory allocation, which is crucial to providing 210 | [exception safety](#exception-safety). Code the following instead. 211 | 212 | ~~~C++ 213 | auto p = allocate(); 214 | init(p, ...); 215 | ~~~ 216 | 217 | `LF_MAKE(p, T, ...)` is provided to help with this. 218 | 219 | -------------------------------------------------------------------------------- 220 | 221 | ~~~C++ 222 | template 223 | T emplace(Us&&... us); 224 | ~~~ 225 | 226 | Creates a `T` on call stack with [introspective initialization](#introspective-initialization) from `us...`. 227 | Note that `T` is not required to be copyable nor movable, since [RVO][4] is mandatory since C++17. 228 | 229 | -------------------------------------------------------------------------------- 230 | 231 | ~~~C++ 232 | LF_MAKE(p, T, ...) 233 | ~~~ 234 | 235 | This helper macro combines memory allocation and object initialization. 236 | It guarantees that the initializer expression is evaluated after memory allocation to guard 237 | against memory allocation exception [[ref](#exception-safety)]). 238 | Specifically, it expands to 239 | 240 | ~~~C++ 241 | auto p = allocate(); 242 | init(p ...); 243 | ~~~ 244 | 245 | Note that if initializer expression is non-empty, double commas are needed after `T`, e.g., `LF_MAKE(p, T,, a, b);` 246 | If initializer expression is empty, single comma is needed, e.g., `LF_MAKE(p, T,);` 247 | 248 | -------------------------------------------------------------------------------- 249 | 250 | ~~~C++ 251 | template 252 | using unique_ptr = std::unique_ptr; 253 | 254 | template 255 | auto init_unique(T* p, Us&&... us); 256 | 257 | LF_MAKE_UNIQUE(p, T, ...) 258 | ~~~ 259 | 260 | `init_unique()` is similar to `init()` but additionally returns a `unique_ptr`. 261 | Note that do not code `init_unique(allocate(), ...)`. Since argument evaluation order is [unspecified][6], the code does not guarantee that the initializer expression is evaluated after memory allocation, 262 | which is crucial to providing [exception safety](#exception-safety). Code the following instead. 263 | 264 | ~~~C++ 265 | auto p = allocate(); 266 | init_unique(p, ...); 267 | ~~~ 268 | 269 | `LF_MAKE_UNIQUE(p, T, ...)` expands to 270 | 271 | ~~~C++ 272 | auto some_unique_name = allocate(); 273 | auto p = init_unique(some_unique_name ...); 274 | ~~~ 275 | 276 | Note that if initializer expression is non-empty, double commas are needed after `T`, 277 | e.g., `LF_MAKE_UNIQUE(p, T,, a, b);` If initializer expression is empty, single comma is needed, 278 | e.g., `LF_MAKE_UNIQUE(p, T,);` 279 | 280 | [1]:https://stackoverflow.com/q/49546754/1348273 281 | [2]:http://en.cppreference.com/w/cpp/memory/new/operator_new 282 | [3]:https://stackoverflow.com/q/49568858/1348273 283 | [4]:http://en.cppreference.com/w/cpp/language/copy_elision 284 | [5]:https://stackoverflow.com/q/49646113/1348273 285 | [6]:http://en.cppreference.com/w/cpp/language/eval_order 286 | --------------------------------------------------------------------------------