From 1af728b505ec1340be786024504ef3b6eb7a53fc Mon Sep 17 00:00:00 2001 From: Matej Ferencevic Date: Mon, 14 Jan 2019 11:11:51 +0100 Subject: [PATCH] Implement new SkipList Reviewers: teon.banek, msantl, ipaljak Reviewed By: teon.banek Subscribers: pullbot Differential Revision: https://phabricator.memgraph.io/D1787 --- src/utils/linux.hpp | 10 + src/utils/skip_list.hpp | 921 ++++++++++++++++++ src/utils/stack.hpp | 114 +++ tests/benchmark/CMakeLists.txt | 12 + tests/benchmark/skip_list_common.hpp | 79 ++ tests/benchmark/skip_list_random.cpp | 61 ++ tests/benchmark/skip_list_real_world.cpp | 67 ++ tests/benchmark/skip_list_same_item.cpp | 41 + tests/benchmark/skip_list_vs_stl.cpp | 316 ++++++ tests/concurrent/CMakeLists.txt | 18 + tests/concurrent/skip_list_insert.cpp | 36 + .../skip_list_insert_competitive.cpp | 42 + tests/concurrent/skip_list_mixed.cpp | 84 ++ tests/concurrent/skip_list_remove.cpp | 43 + .../skip_list_remove_competitive.cpp | 49 + tests/concurrent/stack.cpp | 66 ++ tests/unit/CMakeLists.txt | 3 + tests/unit/skip_list.cpp | 391 ++++++++ 18 files changed, 2353 insertions(+) create mode 100644 src/utils/linux.hpp create mode 100644 src/utils/skip_list.hpp create mode 100644 src/utils/stack.hpp create mode 100644 tests/benchmark/skip_list_common.hpp create mode 100644 tests/benchmark/skip_list_random.cpp create mode 100644 tests/benchmark/skip_list_real_world.cpp create mode 100644 tests/benchmark/skip_list_same_item.cpp create mode 100644 tests/benchmark/skip_list_vs_stl.cpp create mode 100644 tests/concurrent/skip_list_insert.cpp create mode 100644 tests/concurrent/skip_list_insert_competitive.cpp create mode 100644 tests/concurrent/skip_list_mixed.cpp create mode 100644 tests/concurrent/skip_list_remove.cpp create mode 100644 tests/concurrent/skip_list_remove_competitive.cpp create mode 100644 tests/concurrent/stack.cpp create mode 100644 tests/unit/skip_list.cpp diff --git a/src/utils/linux.hpp b/src/utils/linux.hpp new file mode 100644 index 000000000..835d15bc8 --- /dev/null +++ b/src/utils/linux.hpp @@ -0,0 +1,10 @@ +#pragma once + +namespace utils { + +// This is the default Linux page size found on all architectures. The proper +// way to check for this constant is to call `sysconf(_SC_PAGESIZE)`, but we +// can't use that as a `constexpr`. +const uint64_t kLinuxPageSize = 4096; + +} // namespace utils diff --git a/src/utils/skip_list.hpp b/src/utils/skip_list.hpp new file mode 100644 index 000000000..d32612724 --- /dev/null +++ b/src/utils/skip_list.hpp @@ -0,0 +1,921 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "utils/linux.hpp" +#include "utils/on_scope_exit.hpp" +#include "utils/spin_lock.hpp" +#include "utils/stack.hpp" + +namespace utils { + +/// This is the maximum height of the list. This value shouldn't be changed from +/// this value because it isn't practical to have skip lists that have larger +/// heights than 32. The probability of heights larger than 32 gets extremely +/// small. Also, the internal implementation can handle a maximum height of 32 +/// primarily becase of the height generator (see the `gen_height` function). +const uint64_t kSkipListMaxHeight = 32; + +/// This is the height that a node that is accessed from the list has to have in +/// order for garbage collection to be triggered. This causes the garbage +/// collection to be probabilistically triggered on each operation with the +/// list. Each thread that accesses the list can perform garbage collection. The +/// level is determined empirically using benchmarks. A smaller height means +/// that the garbage collection will be triggered more often. +const uint64_t kSkipListGcHeightTrigger = 16; + +/// These variables define the storage sizes for the SkipListGc. The internal +/// storage of the GC and the Stack storage used within the GC are all +/// optimized to have block sizes that are a whole multiple of the memory page +/// size. +const uint64_t kSkipListGcBlockSize = 8189; +const uint64_t kSkipListGcStackSize = 8191; + +/// This is the Node object that represents each element stored in the list. The +/// array of pointers to the next nodes is declared here to a size of 0 so that +/// we can dynamically allocate different sizes of memory for nodes of different +/// heights. Because the array is of length 0 its size is also 0. That means +/// that `sizeof(SkipListNode)` is equal to the size of all members +/// without the `nexts` member. When we allocate memory for the node we allocate +/// `sizeof(SkipListNode) + height * sizeof(std::atomic +/// *>)`. +/// +/// The allocated memory is then used like this: +/// [ Node ][Node*][Node*][Node*]...[Node*] +/// | | | | | +/// | 0 1 2 height-1 +/// |----------------||-----------------------------| +/// space for the space for pointers +/// Node structure to the next Node +template +struct SkipListNode { + SkipListNode(uint8_t _height) : height(_height) {} + + SkipListNode(const TObj &_obj, uint8_t _height) + : obj(_obj), height(_height) {} + + SkipListNode(TObj &&_obj, uint8_t _height) + : obj(std::move(_obj)), height(_height) {} + + // The items here are carefully placed to minimize padding gaps. + + TObj obj; + SpinLock lock; + std::atomic marked; + std::atomic fully_linked; + uint8_t height; + // uint8_t PAD; + std::atomic *> nexts[0]; +}; + +/// The skip list doesn't have built-in reclamation of removed nodes (objects). +/// This class handles all operations necessary to remove the nodes safely. +/// +/// The principal of operation is as follows: +/// Each accessor to the skip list is given an ID. When nodes are garbage +/// collected the ID of the currently newest living accessor is recorded. When +/// that accessor is destroyed the node can be safely destroyed. +/// This is correct because when the skip list removes the node it immediately +/// unlinks it from the structure so no new accessors can reach it. The last +/// possible accessor that can still have a reference to the removed object is +/// the currently living accessor that has the largest ID. +/// +/// To enable fast operations this GC stores accessor IDs in a specially crafted +/// structure. It consists of a doubly-linked list of Blocks. Each Block holds +/// the information (alive/dead) for about 500k accessors. When an accessor is +/// destroyed the corresponding bit for the accessor is found in the list and is +/// set. When garbage collection occurs it finds the largest prefix of dead +/// accessors and destroys all nodes that have the largest living accessor ID +/// corresponding to them less than the largest currently found dead accessor. +/// +/// Insertion into the dead accessor list is fast because the blocks are large +/// and the corresponding bit can be set atomically. The only times when the +/// collection is blocking is when the structure of the doubly-linked list has +/// to be changed (eg. a new Block has to be allocated and linked into the +/// structure). +template +class SkipListGc final { + private: + using TNode = SkipListNode; + using TDeleted = std::pair; + using TStack = Stack; + + const uint64_t kIdsInField = sizeof(uint64_t) * 8; + const uint64_t kIdsInBlock = kSkipListGcBlockSize * kIdsInField; + + struct Block { + std::atomic prev; + std::atomic succ; + uint64_t first_id; + std::atomic field[kSkipListGcBlockSize]; + }; + + Block *AllocateBlock(Block *head) { + std::lock_guard guard(lock_); + Block *curr_head = head_.load(std::memory_order_relaxed); + if (curr_head == head) { + Block *block = new (calloc(1, sizeof(Block))) Block(); + block->prev.store(curr_head, std::memory_order_relaxed); + block->succ.store(nullptr, std::memory_order_relaxed); + block->first_id = last_id_; + last_id_ += kIdsInBlock; + if (curr_head == nullptr) { + tail_.store(block, std::memory_order_relaxed); + } else { + curr_head->succ.store(block, std::memory_order_relaxed); + } + head_.store(block, std::memory_order_relaxed); + return block; + } else { + return curr_head; + } + } + + public: + SkipListGc() { + static_assert(sizeof(Block) % kLinuxPageSize == 0, + "It is recommended that you set the kSkipListGcBlockSize " + "constant so that the size of SkipListGc::Block is a " + "multiple of the page size."); + } + + SkipListGc(const SkipListGc &) = delete; + SkipListGc &operator=(const SkipListGc &) = delete; + SkipListGc(SkipListGc &&other) = delete; + SkipListGc &operator=(SkipListGc &&other) = delete; + + ~SkipListGc() { + Block *head = head_.load(std::memory_order_relaxed); + while (head != nullptr) { + Block *prev = head->prev.load(std::memory_order_relaxed); + head->~Block(); + free(head); + head = prev; + } + std::experimental::optional item; + while ((item = deleted_.Pop())) { + item->second->~TNode(); + free(item->second); + } + } + + uint64_t AllocateId() { + return accessor_id_.fetch_add(1, std::memory_order_relaxed); + } + + void ReleaseId(uint64_t id) { + // This function only needs to acquire a lock when allocating a new block + // (in the `AllocateBlock` function), but otherwise doesn't need to acquire + // a lock because it iterates over the linked list and atomically sets its + // 'dead' bit in the block field. The structure of the linked list can be + // accessed without a lock because all of the pointers in the list are + // atomic and their modification is done so that the access is always + // correct. + Block *head = head_.load(std::memory_order_relaxed); + if (head == nullptr) { + head = AllocateBlock(head); + } + while (true) { + CHECK(head != nullptr) << "Missing SkipListGc block!"; + if (id < head->first_id) { + head = head->prev.load(std::memory_order_relaxed); + } else if (id >= head->first_id + kIdsInBlock) { + head = AllocateBlock(head); + } else { + id -= head->first_id; + uint64_t field = id / kIdsInField; + uint64_t bit = id % kIdsInField; + uint64_t value = 1; + value <<= bit; + auto ret = + head->field[field].fetch_or(value, std::memory_order_relaxed); + CHECK(!(ret & value)) << "A SkipList Accessor was released twice!"; + break; + } + } + } + + void Collect(TNode *node) { + std::lock_guard guard(lock_); + deleted_.Push({accessor_id_.load(std::memory_order_relaxed), node}); + } + + void Run() { + if (!lock_.try_lock()) return; + OnScopeExit cleanup([&] { lock_.unlock(); }); + Block *tail = tail_.load(std::memory_order_relaxed); + uint64_t last_dead = 0; + bool remove_block = true; + while (tail != nullptr && remove_block) { + for (uint64_t pos = 0; pos < kSkipListGcBlockSize; ++pos) { + uint64_t field = tail->field[pos].load(std::memory_order_relaxed); + if (field != std::numeric_limits::max()) { + if (field != 0) { + // Here we find the position of the least significant zero bit + // (using a inverted value and the `ffs` function to find the + // position of the first set bit). We find this position because we + // know that all bits that are of less significance are then all + // ones. That means that the `where_alive` will be the first ID that + // is still alive. That means that we have a prefix of all dead + // accessors that have IDs less than `where_alive`. + int where_alive = __builtin_ffsl(~field) - 1; + if (where_alive > 0) { + last_dead = tail->first_id + pos * kIdsInField + where_alive - 1; + } + } + remove_block = false; + break; + } else { + last_dead = tail->first_id + (pos + 1) * kIdsInField - 1; + } + } + Block *next = tail->succ.load(std::memory_order_relaxed); + // Here we also check whether the next block isn't a `nullptr`. If it is + // `nullptr` that means that this is the last existing block. We don't + // want to destroy it because we would need to change the `head_` to point + // to `nullptr`. We can't do that because we can't guarantee that some + // thread doesn't have a pointer to the block that it got from reading + // `head_`. We bail out here, this block will be freed next time. + if (remove_block && next != nullptr) { + CHECK(tail == tail_.load(std::memory_order_relaxed)) + << "Can't remove SkipListGc block that is in the middle!"; + next->prev.store(nullptr, std::memory_order_relaxed); + tail_.store(next, std::memory_order_relaxed); + // Destroy the block. + tail->~Block(); + free(tail); + } + tail = next; + } + TStack leftover; + std::experimental::optional item; + while ((item = deleted_.Pop())) { + if (item->first < last_dead) { + item->second->~TNode(); + free(item->second); + } else { + leftover.Push(*item); + } + } + while ((item = leftover.Pop())) { + deleted_.Push(*item); + } + } + + private: + SpinLock lock_; + std::atomic accessor_id_{0}; + std::atomic head_{nullptr}; + std::atomic tail_{nullptr}; + uint64_t last_id_{0}; + TStack deleted_; +}; + +/// Concurrent skip list. It is mostly lock-free and fine-grained locking is +/// used for conflict resolution. +/// +/// The implementation is based on the work described in the paper +/// "A Provably Correct Scalable Concurrent Skip List" +/// https://www.cs.tau.ac.il/~shanir/nir-pubs-web/Papers/OPODIS2006-BA.pdf +/// +/// The proposed implementation is in Java so the authors don't worry about +/// garbage collection. This implementation uses the garbage collector that is +/// described previously in this file. The garbage collection is triggered +/// occasionally on each operation that is executed on the list through an +/// accessor. +/// +/// The implementation has an interface which mimics the interface of STL +/// containers like `std::set` and `std::map`. +/// +/// When getting iterators to contained objects it is guaranteed that the +/// iterator will point to a living object while the accessor that produced it +/// is alive. So to say it differently, you *MUST* keep the accessor to the list +/// alive while you still have some iterators to items that are contained in the +/// list. That is the only usage in which the list guarantees that the item that +/// is pointed to by the iterator doesn't get deleted (freed) by another thread. +/// +/// +/// The first use-case for the skip list is when you want to use the list as a +/// `std::set`. The items are stored sorted and duplicates aren't permitted. +/// +/// utils::SkipList list; +/// { +/// auto accessor = list.access(); +/// +/// // Inserts item into the skiplist and returns an pair. +/// // The Iterator points to the newly created node and the boolean is +/// // `true` indicating that the item was inserted. +/// accessor.insert(42); +/// +/// // Nothing gets inserted because 42 already exist in the list. The +/// // returned iterator points to the existing item in the list and the +/// // boolean is `false` indicating that the insertion failed (an existing +/// // item was returned). +/// accessor.insert(42); +/// +/// // Returns an iterator to the element 42. +/// auto it = accessor.find(42); +/// +/// // Returns an empty iterator. The iterator is equal to `accessor.end()`. +/// auto it = accessor.find(99); +/// +/// // Iterate over all items in the list. +/// for (auto it = accessor.begin(); it != accessor.end(); ++it) { +/// std::cout << *it << std::endl; +/// } +/// +/// // The loop can also be range based. +/// for (auto &e : accessor) { +/// std::cout << e << std::endl; +/// } +/// +/// // This removes the existing element from the list and returns `true` +/// // indicating that the removal was successful. +/// accessor.remove(42); +/// +/// // This returns `false` indicating that the removal failed because the +/// // item doesn't exist in the list. +/// accessor.remove(42); +/// } +/// +/// // The accessor is out-of-scope and the 42 object can be deleted (freed) +/// // from main memory now that there is no alive accessor that can +/// // reach it anymore. +/// +/// +/// In order to use a custom type for the set use-case you have to implement the +/// following operators: +/// +/// bool operator==(const custom &a, const custom &b); +/// bool operator<(const custom &a, const custom &b); +/// +/// +/// Another use-case for the skip list is when you want to use the list as a +/// `std::map`. The items are stored sorted and duplicates aren't permitted. +/// +/// struct MapObject { +/// uint64_t key; +/// std::string value; +/// }; +/// +/// bool operator==(const MapObject &a, const MapObject &b) { +/// return a.key == b.key; +/// } +/// bool operator<(const MapObject &a, const MapObject &b) { +/// return a.key < b.key; +/// } +/// +/// +/// When using the list as a map it is recommended that the object stored in the +/// list has a single field that is the "key" of the map. It is also recommended +/// that the key field is the first field in the object. The comparison +/// operators should then only use the key field for comparison. +/// +/// utils::SkipList list; +/// { +/// auto accessor = list.access(); +/// +/// // Inserts an object into the list. +/// accessor.insert(MapObject{5, "hello world"}); +/// +/// // This operation will return an iterator that isn't equal to +/// // `accessor.end()`. This is because the comparison operators only use +/// // the key field for comparison, the value field is ignored. +/// accessor.find(MapObject{5, "this probably isn't desired"}); +/// +/// // This will also succeed in removing the object. +/// accessor.remove(MapObject{5, "not good"}); +/// } +/// +/// +/// If you want to be able to query the list using only the key field you have +/// to implement two more comparison operators. Those are: +/// +/// bool operator==(const MapObject &a, const uint64_t &b) { +/// return a.key == b; +/// } +/// bool operator<(const MapObject &a, const uint64_t &b) { +/// return a.key < b; +/// } +/// +/// +/// Now you can use the list in a more intuitive way (as a map): +/// +/// { +/// auto accessor = list.access(); +/// +/// // Inserts an object into the list. +/// accessor.insert({5, "hello world"}); +/// +/// // This successfully finds the inserted object. +/// accessor.find(5); +/// +/// // This successfully removes the inserted object. +/// accessor.remove(5); +/// } +/// +/// +/// For detailed documentation about the operations available, see the +/// documentation in the Accessor class. +/// +/// Note: When using the SkipList as a replacement for `std::set` and searching +/// for some object in the list with `find` you will get an `Iterator` to the +/// object that will allow you to change the contents to the object. Do *not* +/// change the contents of the object! If you change the contents of the object +/// the structure of the SkipList will be compromised! +/// +/// Note: When using the SkipList as a replacement for `std::map` and searching +/// for some object in the list the same restriction applies as for the +/// `std::set` use-case. The only difference here is that you must *not* change +/// the key of the object. The value can be modified, but you must have in mind +/// that the change can be done from multiple threads simultaneously so the +/// change must be implemented thread-safe inside the object. +/// +/// @tparam TObj object type that is stored in the list +template +class SkipList final { + private: + using TNode = SkipListNode; + + public: + class ConstIterator; + + class Iterator final { + private: + friend class SkipList; + friend class ConstIterator; + + Iterator(TNode *node) : node_(node) {} + + public: + TObj &operator*() const { return node_->obj; } + + TObj *operator->() const { return &node_->obj; } + + bool operator==(const Iterator &other) const { + return node_ == other.node_; + } + + bool operator!=(const Iterator &other) const { + return node_ != other.node_; + } + + Iterator &operator++() { + while (true) { + TNode *next = node_->nexts[0].load(std::memory_order_relaxed); + if (next == nullptr || !next->marked.load(std::memory_order_relaxed)) { + node_ = next; + return *this; + } + } + } + + private: + TNode *node_; + }; + + class ConstIterator final { + private: + friend class SkipList; + + ConstIterator(TNode *node) : node_(node) {} + + public: + ConstIterator(const Iterator &it) : node_(it.node_) {} + + const TObj &operator*() const { return node_->obj; } + + const TObj *operator->() const { return &node_->obj; } + + bool operator==(const ConstIterator &other) const { + return node_ == other.node_; + } + + bool operator!=(const ConstIterator &other) const { + return node_ != other.node_; + } + + ConstIterator &operator++() { + while (true) { + TNode *next = node_->nexts[0].load(std::memory_order_relaxed); + if (next == nullptr || !next->marked.load(std::memory_order_relaxed)) { + node_ = next; + return *this; + } + } + } + + private: + TNode *node_; + }; + + class Accessor final { + private: + friend class SkipList; + + explicit Accessor(SkipList *skiplist) + : skiplist_(skiplist), id_(skiplist->gc_.AllocateId()) {} + + public: + ~Accessor() { + if (skiplist_ != nullptr) skiplist_->gc_.ReleaseId(id_); + } + + Accessor(const Accessor &other) + : skiplist_(other.skiplist_), id_(skiplist_->gc_.AllocateId()) {} + Accessor(Accessor &&other) : skiplist_(other.skiplist_), id_(other.id_) { + other.skiplist_ = nullptr; + } + Accessor &operator=(const Accessor &other) { + skiplist_ = other.skiplist_; + id_ = skiplist_->gc_.AllocateId(); + } + Accessor &operator=(Accessor &&other) { + skiplist_ = other.skiplist_; + id_ = other.id_; + other.skiplist_ = nullptr; + } + + /// Functions that return an Iterator (or ConstIterator) to the beginning of + /// the list. + Iterator begin() { + return Iterator{ + skiplist_->head_->nexts[0].load(std::memory_order_relaxed)}; + } + ConstIterator begin() const { + return ConstIterator{ + skiplist_->head_->nexts[0].load(std::memory_order_relaxed)}; + } + ConstIterator cbegin() const { + return ConstIterator{ + skiplist_->head_->nexts[0].load(std::memory_order_relaxed)}; + } + + /// Functions that return an Iterator (or ConstIterator) to the end of the + /// list. + Iterator end() { return Iterator{nullptr}; } + ConstIterator end() const { return ConstIterator{nullptr}; } + ConstIterator cend() const { return ConstIterator{nullptr}; } + + std::pair insert(const TObj &object) { + return skiplist_->insert(object); + } + + /// Inserts an object into the list. It returns an iterator to the item that + /// is in the list. If the item already exists in the list no insertion is + /// done and an iterator to the existing item is returned. + /// + /// @return Iterator to the item that is in the list + /// bool indicates whether the item was inserted into the list + std::pair insert(TObj &&object) { + return skiplist_->insert(std::move(object)); + } + + /// Checks whether the key exists in the list. + /// + /// @return bool indicating whether the item exists + template + bool contains(const TKey &key) const { + return skiplist_->template contains(key); + } + + /// Finds the key in the list and returns an iterator to the item. + /// + /// @return Iterator to the item in the list, will be equal to `end()` when + /// the key isn't found + template + Iterator find(const TKey &key) { + return skiplist_->template find(key); + } + + /// Removes the key from the list. + /// + /// @return bool indicating whether the removal was successful + template + bool remove(const TKey &key) { + return skiplist_->template remove(key); + } + + /// Returns the number of items contained in the list. + /// + /// @return size of the list + uint64_t size() const { return skiplist_->size(); } + + private: + SkipList *skiplist_{nullptr}; + uint64_t id_{0}; + }; + + class ConstAccessor final { + private: + friend class SkipList; + + explicit ConstAccessor(const SkipList *skiplist) + : skiplist_(skiplist), id_(skiplist->gc_.AllocateId()) {} + + public: + ~ConstAccessor() { + if (skiplist_ != nullptr) skiplist_->gc_.ReleaseId(id_); + } + + ConstAccessor(const ConstAccessor &other) + : skiplist_(other.skiplist_), id_(skiplist_->gc_.AllocateId()) {} + ConstAccessor(ConstAccessor &&other) + : skiplist_(other.skiplist_), id_(other.id_) { + other.skiplist_ = nullptr; + } + ConstAccessor &operator=(const ConstAccessor &other) { + skiplist_ = other.skiplist_; + id_ = skiplist_->gc_.AllocateId(); + } + ConstAccessor &operator=(ConstAccessor &&other) { + skiplist_ = other.skiplist_; + id_ = other.id_; + other.skiplist_ = nullptr; + } + + ConstIterator begin() const { + return ConstIterator{ + skiplist_->head_->nexts[0].load(std::memory_order_relaxed)}; + } + ConstIterator cbegin() const { + return ConstIterator{ + skiplist_->head_->nexts[0].load(std::memory_order_relaxed)}; + } + + ConstIterator end() const { return ConstIterator{nullptr}; } + ConstIterator cend() const { return ConstIterator{nullptr}; } + + template + bool contains(const TKey &key) const { + return skiplist_->template contains(key); + } + + template + ConstIterator find(const TKey &key) const { + return skiplist_->template find(key); + } + + uint64_t size() const { return skiplist_->size(); } + + private: + const SkipList *skiplist_{nullptr}; + uint64_t id_{0}; + }; + + SkipList() { + static_assert(kSkipListMaxHeight <= 32, + "The SkipList height must be less or equal to 32!"); + // Here we use `calloc` instead of `malloc` to ensure that the memory is + // filled with zeros before we call the constructor. We don't use `malloc` + + // `memset` because `calloc` is smarter: + // https://stackoverflow.com/questions/2688466/why-mallocmemset-is-slower-than-calloc/2688522#2688522 + head_ = new (calloc( + 1, sizeof(TNode) + kSkipListMaxHeight * sizeof(std::atomic))) + SkipListNode(kSkipListMaxHeight); + } + + SkipList(SkipList &&other) : head_(other.head_), size_(other.size_.load()) { + other.head_ = nullptr; + } + SkipList &operator=(SkipList &&other) { + TNode *head = head_; + while (head != nullptr) { + TNode *succ = head->nexts[0].load(std::memory_order_relaxed); + head->~TNode(); + free(head); + head = succ; + } + head_ = other.head_; + size_ = other.size_.load(); + other.head_ = nullptr; + return *this; + } + + SkipList(const SkipList &) = delete; + SkipList &operator=(const SkipList &) = delete; + + ~SkipList() { + TNode *head = head_; + while (head != nullptr) { + TNode *succ = head->nexts[0].load(std::memory_order_relaxed); + head->~TNode(); + free(head); + head = succ; + } + } + + /// Functions that return an accessor to the list. All operations on the list + /// must be done through the Accessor (or ConstAccessor) proxy object. + Accessor access() { return Accessor{this}; } + ConstAccessor access() const { return ConstAccessor{this}; } + + /// The size of the list can be read directly from the list because it is an + /// atomic operation. + uint64_t size() const { return size_.load(std::memory_order_relaxed); } + + private: + template + int find_node(const TKey &key, TNode *preds[], TNode *succs[]) const { + int layer_found = -1; + TNode *pred = head_; + for (int layer = kSkipListMaxHeight - 1; layer >= 0; --layer) { + TNode *curr = pred->nexts[layer].load(std::memory_order_relaxed); + // Existence test is missing in the paper. + while (curr != nullptr && curr->obj < key) { + pred = curr; + curr = pred->nexts[layer].load(std::memory_order_relaxed); + } + // Existence test is missing in the paper. + if (layer_found == -1 && curr && curr->obj == key) { + layer_found = layer; + } + preds[layer] = pred; + succs[layer] = curr; + } + if (layer_found + 1 >= kSkipListGcHeightTrigger) gc_.Run(); + return layer_found; + } + + template + std::pair insert(TObjUniv &&object) { + int top_layer = gen_height(); + TNode *preds[kSkipListMaxHeight], *succs[kSkipListMaxHeight]; + if (top_layer >= kSkipListGcHeightTrigger) gc_.Run(); + while (true) { + int layer_found = find_node(object, preds, succs); + if (layer_found != -1) { + TNode *node_found = succs[layer_found]; + if (!node_found->marked.load(std::memory_order_relaxed)) { + while (!node_found->fully_linked.load(std::memory_order_relaxed)) + ; + return {Iterator{node_found}, false}; + } + continue; + } + + std::unique_lock guards[kSkipListMaxHeight]; + TNode *pred, *succ, *prev_pred = nullptr; + bool valid = true; + // The paper has a wrong condition here. In the paper it states that this + // loop should have `(layer <= top_layer)`, but that isn't correct. + for (int layer = 0; valid && (layer < top_layer); ++layer) { + pred = preds[layer]; + succ = succs[layer]; + if (pred != prev_pred) { + guards[layer] = std::unique_lock(pred->lock); + prev_pred = pred; + } + // Existence test is missing in the paper. + valid = + !pred->marked.load(std::memory_order_relaxed) && + pred->nexts[layer].load(std::memory_order_relaxed) == succ && + (succ == nullptr || !succ->marked.load(std::memory_order_relaxed)); + } + + if (!valid) continue; + + // Here we use `calloc` instead of `malloc` to ensure that the memory is + // filled with zeros before we call the constructor. We don't use `malloc` + // + `memset` because `calloc` is smarter: + // https://stackoverflow.com/questions/2688466/why-mallocmemset-is-slower-than-calloc/2688522#2688522 + TNode *new_node = new ( + calloc(1, sizeof(TNode) + top_layer * sizeof(std::atomic))) + TNode(std::forward(object), top_layer); + + // The paper is also wrong here. It states that the loop should go up to + // `top_layer` which is wrong. + for (int layer = 0; layer < top_layer; ++layer) { + new_node->nexts[layer].store(succs[layer], std::memory_order_relaxed); + preds[layer]->nexts[layer].store(new_node, std::memory_order_relaxed); + } + + new_node->fully_linked.store(true, std::memory_order_relaxed); + size_.fetch_add(1, std::memory_order_relaxed); + return {Iterator{new_node}, true}; + } + } + + template + bool contains(const TKey &key) const { + TNode *preds[kSkipListMaxHeight], *succs[kSkipListMaxHeight]; + int layer_found = find_node(key, preds, succs); + return (layer_found != -1 && + succs[layer_found]->fully_linked.load(std::memory_order_relaxed) && + !succs[layer_found]->marked.load(std::memory_order_relaxed)); + } + + template + Iterator find(const TKey &key) const { + TNode *preds[kSkipListMaxHeight], *succs[kSkipListMaxHeight]; + int layer_found = find_node(key, preds, succs); + if (layer_found != -1 && + succs[layer_found]->fully_linked.load(std::memory_order_relaxed) && + !succs[layer_found]->marked.load(std::memory_order_relaxed)) { + return Iterator{succs[layer_found]}; + } + return Iterator{nullptr}; + } + + bool ok_to_delete(TNode *candidate, int layer_found) { + // The paper has an incorrect check here. It expects the `layer_found` + // variable to be 1-indexed, but in fact it is 0-indexed. + return (candidate->fully_linked.load(std::memory_order_relaxed) && + candidate->height == layer_found + 1 && + !candidate->marked.load(std::memory_order_relaxed)); + } + + template + bool remove(const TKey &key) { + TNode *node_to_delete = nullptr; + bool is_marked = false; + int top_layer = -1; + TNode *preds[kSkipListMaxHeight], *succs[kSkipListMaxHeight]; + std::unique_lock node_guard; + while (true) { + int layer_found = find_node(key, preds, succs); + if (is_marked || (layer_found != -1 && + ok_to_delete(succs[layer_found], layer_found))) { + if (!is_marked) { + node_to_delete = succs[layer_found]; + top_layer = node_to_delete->height; + node_guard = std::unique_lock(node_to_delete->lock); + if (node_to_delete->marked.load(std::memory_order_relaxed)) { + return false; + } + node_to_delete->marked.store(true, std::memory_order_relaxed); + is_marked = true; + } + + std::unique_lock guards[kSkipListMaxHeight]; + TNode *pred, *succ, *prev_pred = nullptr; + bool valid = true; + // The paper has a wrong condition here. In the paper it states that + // this loop should have `(layer <= top_layer)`, but that isn't correct. + for (int layer = 0; valid && (layer < top_layer); ++layer) { + pred = preds[layer]; + succ = succs[layer]; + if (pred != prev_pred) { + guards[layer] = std::unique_lock(pred->lock); + prev_pred = pred; + } + valid = !pred->marked.load(std::memory_order_relaxed) && + pred->nexts[layer].load(std::memory_order_relaxed) == succ; + } + + if (!valid) continue; + + // The paper is also wrong here. It states that the loop should start + // from `top_layer` which is wrong. + for (int layer = top_layer - 1; layer >= 0; --layer) { + preds[layer]->nexts[layer].store( + node_to_delete->nexts[layer].load(std::memory_order_relaxed), + std::memory_order_relaxed); + } + gc_.Collect(node_to_delete); + size_.fetch_add(-1, std::memory_order_relaxed); + return true; + } else { + return false; + } + } + } + + // This function generates a binomial distribution using the same technique + // described here: http://ticki.github.io/blog/skip-lists-done-right/ under + // "O(1) level generation". The only exception in this implementation is that + // the special case of 0 is handled correctly. When 0 is passed to `ffs` it + // returns 0 which is an invalid height. To make the distribution binomial + // this value is then mapped to `kSkipListMaxSize`. + uint32_t gen_height() { + std::lock_guard guard(lock_); + static_assert(kSkipListMaxHeight <= 32, + "utils::SkipList::gen_height is implemented only for heights " + "up to 32!"); + uint32_t value = gen_(); + if (value == 0) return kSkipListMaxHeight; + // The value should have exactly `kSkipListMaxHeight` bits. + value >>= (32 - kSkipListMaxHeight); + // ffs = find first set + // ^ ^ ^ + return __builtin_ffs(value); + } + + private: + TNode *head_{nullptr}; + mutable SkipListGc gc_; + + std::mt19937 gen_{std::random_device{}()}; + std::atomic size_{0}; + SpinLock lock_; +}; + +} // namespace utils diff --git a/src/utils/stack.hpp b/src/utils/stack.hpp new file mode 100644 index 000000000..a0d767e66 --- /dev/null +++ b/src/utils/stack.hpp @@ -0,0 +1,114 @@ +#pragma once + +#include +#include + +#include + +#include "utils/linux.hpp" +#include "utils/spin_lock.hpp" + +namespace utils { + +/// This class implements a stack. It is primarily intended for storing +/// primitive types. This stack is thread-safe. It uses a spin lock to lock all +/// operations. +/// +/// The stack stores all objects in memory blocks. That makes it really +/// efficient in terms of memory allocations. The memory block size is optimized +/// to be a multiple of the Linux memory page size so that only entire memory +/// pages are used. The constructor has a static_assert to warn you that you are +/// not using a valid `TSize` parameter. The `TSize` parameter should make the +/// `struct Block` a multiple in size of the Linux memory page size. +/// +/// This can be calculated using: +/// sizeof(Block *) + sizeof(uint64_t) + sizeof(TObj) * TSize \ +/// == k * kLinuxPageSize +/// +/// Which translates to: +/// 8 + 8 + sizeof(TObj) * TSize == k * 4096 +/// +/// @tparam TObj primitive object that should be stored in the stack +/// @tparam TSize size of the memory block used +template +class Stack { + private: + struct Block { + Block *prev{nullptr}; + uint64_t used{0}; + TObj obj[TSize]; + }; + + public: + Stack() { + static_assert(sizeof(Block) % kLinuxPageSize == 0, + "It is recommended that you set the TSize constant so that " + "the size of Stack::Block is a multiple of the page size."); + } + + Stack(Stack &&other) : head_(other.head_) { other.head_ = nullptr; } + Stack &operator=(Stack &&other) { + while (head_ != nullptr) { + Block *prev = head_->prev; + delete head_; + head_ = prev; + } + head_ = other.head_; + other.head_ = nullptr; + return *this; + } + + Stack(const Stack &) = delete; + Stack &operator=(const Stack &) = delete; + + ~Stack() { + while (head_ != nullptr) { + Block *prev = head_->prev; + delete head_; + head_ = prev; + } + } + + void Push(TObj obj) { + std::lock_guard guard(lock_); + if (head_ == nullptr) { + // Allocate a new block. + head_ = new Block(); + } + while (true) { + CHECK(head_->used <= TSize) << "utils::Stack has more elements in a " + "Block than the block has space!"; + if (head_->used == TSize) { + // Allocate a new block. + Block *block = new Block(); + block->prev = head_; + head_ = block; + } else { + head_->obj[head_->used++] = obj; + break; + } + } + } + + std::experimental::optional Pop() { + std::lock_guard guard(lock_); + while (true) { + if (head_ == nullptr) return std::experimental::nullopt; + CHECK(head_->used <= TSize) << "utils::Stack has more elements in a " + "Block than the block has space!"; + if (head_->used == 0) { + Block *prev = head_->prev; + delete head_; + head_ = prev; + } else { + return head_->obj[--head_->used]; + } + } + } + + private: + SpinLock lock_; + Block *head_{nullptr}; +}; + +} // namespace utils diff --git a/tests/benchmark/CMakeLists.txt b/tests/benchmark/CMakeLists.txt index b5e0631c2..9d3532c0f 100644 --- a/tests/benchmark/CMakeLists.txt +++ b/tests/benchmark/CMakeLists.txt @@ -54,6 +54,18 @@ target_link_libraries(${test_prefix}rpc mg-communication) add_benchmark(serialization.cpp) target_link_libraries(${test_prefix}serialization mg-distributed kvstore_dummy_lib) +add_benchmark(skip_list_random.cpp) +target_link_libraries(${test_prefix}skip_list_random mg-utils) + +add_benchmark(skip_list_real_world.cpp) +target_link_libraries(${test_prefix}skip_list_real_world mg-utils) + +add_benchmark(skip_list_same_item.cpp) +target_link_libraries(${test_prefix}skip_list_same_item mg-utils) + +add_benchmark(skip_list_vs_stl.cpp) +target_link_libraries(${test_prefix}skip_list_vs_stl mg-utils) + add_benchmark(tx_engine.cpp) target_link_libraries(${test_prefix}tx_engine mg-single-node kvstore_dummy_lib) diff --git a/tests/benchmark/skip_list_common.hpp b/tests/benchmark/skip_list_common.hpp new file mode 100644 index 000000000..aff964fbf --- /dev/null +++ b/tests/benchmark/skip_list_common.hpp @@ -0,0 +1,79 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include + +DEFINE_int32(num_threads, 8, "Number of concurrent threads"); +DEFINE_int32(duration, 10, "Duration of test (in seconds)"); + +struct Stats { + uint64_t total{0}; + uint64_t succ[4] = {0, 0, 0, 0}; +}; + +const int OP_INSERT = 0; +const int OP_CONTAINS = 1; +const int OP_REMOVE = 2; +const int OP_FIND = 3; + +inline void RunConcurrentTest( + std::function *, Stats *)> test_func) { + std::atomic run{true}; + + std::unique_ptr stats(new Stats[FLAGS_num_threads]); + + std::vector threads; + for (int i = 0; i < FLAGS_num_threads; ++i) { + threads.push_back(std::thread(test_func, &run, &stats.get()[i])); + } + + std::this_thread::sleep_for(std::chrono::seconds(FLAGS_duration)); + + run.store(false, std::memory_order_relaxed); + for (int i = 0; i < FLAGS_num_threads; ++i) { + Stats *tstats = &stats.get()[i]; + threads[i].join(); + std::cout << "Thread " << i << " stats:" << std::endl; + std::cout << " Operations: " << tstats->total << std::endl; + std::cout << " Successful insert: " << tstats->succ[0] << std::endl; + std::cout << " Successful contains: " << tstats->succ[1] << std::endl; + std::cout << " Successful remove: " << tstats->succ[2] << std::endl; + std::cout << " Successful find: " << tstats->succ[3] << std::endl; + } + + std::cout << std::endl; + uint64_t agg[4] = {0, 0, 0, 0}; + for (int i = 0; i < 4; ++i) { + for (int j = 0; j < FLAGS_num_threads; ++j) { + agg[i] += stats.get()[j].succ[i]; + } + } + std::cout << "Successful insert: " << agg[0] << " (" + << agg[0] / FLAGS_duration << " calls/s)" << std::endl; + std::cout << "Successful contains: " << agg[1] << " (" + << agg[1] / FLAGS_duration << " calls/s)" << std::endl; + std::cout << "Successful remove: " << agg[2] << " (" + << agg[2] / FLAGS_duration << " calls/s)" << std::endl; + std::cout << "Successful find: " << agg[3] << " (" << agg[3] / FLAGS_duration + << " calls/s)" << std::endl; + + std::cout << std::endl; + uint64_t tot = 0, tops = 0; + for (int i = 0; i < 4; ++i) { + tot += agg[i]; + } + for (int i = 0; i < FLAGS_num_threads; ++i) { + tops += stats.get()[i].total; + } + std::cout << "Total successful: " << tot << " (" << tot / FLAGS_duration + << " calls/s)" << std::endl; + std::cout << "Total ops: " << tops << " (" << tops / FLAGS_duration + << " calls/s)" << std::endl; +} diff --git a/tests/benchmark/skip_list_random.cpp b/tests/benchmark/skip_list_random.cpp new file mode 100644 index 000000000..2db4eb59a --- /dev/null +++ b/tests/benchmark/skip_list_random.cpp @@ -0,0 +1,61 @@ +#include "skip_list_common.hpp" + +#include "utils/skip_list.hpp" + +DEFINE_int32(max_element, 20000, "Maximum element in the intial list"); +DEFINE_int32(max_range, 2000000, "Maximum range used for the test"); + +int main(int argc, char **argv) { + gflags::ParseCommandLineFlags(&argc, &argv, true); + google::InitGoogleLogging(argv[0]); + + utils::SkipList list; + + { + auto acc = list.access(); + for (uint64_t i = 0; i <= FLAGS_max_element; ++i) { + CHECK(acc.insert(i).second); + } + uint64_t val = 0; + for (auto item : acc) { + CHECK(item == val); + ++val; + } + CHECK(val == FLAGS_max_element + 1); + } + + RunConcurrentTest([&list](auto *run, auto *stats) { + std::mt19937 generator(std::random_device{}()); + std::uniform_int_distribution distribution(0, 3); + std::mt19937 i_generator(std::random_device{}()); + std::uniform_int_distribution i_distribution(0, FLAGS_max_range); + while (run->load(std::memory_order_relaxed)) { + auto value = distribution(generator); + auto accessor = list.access(); + auto item = i_distribution(i_generator); + switch (value) { + case 0: + stats->succ[OP_INSERT] += + static_cast(accessor.insert(item).second); + break; + case 1: + stats->succ[OP_CONTAINS] += + static_cast(accessor.contains(item)); + break; + case 2: + stats->succ[OP_REMOVE] += + static_cast(accessor.remove(item)); + break; + case 3: + stats->succ[OP_FIND] += + static_cast(accessor.find(item) != accessor.end()); + break; + default: + std::terminate(); + } + ++stats->total; + } + }); + + return 0; +} diff --git a/tests/benchmark/skip_list_real_world.cpp b/tests/benchmark/skip_list_real_world.cpp new file mode 100644 index 000000000..5cae01ed5 --- /dev/null +++ b/tests/benchmark/skip_list_real_world.cpp @@ -0,0 +1,67 @@ +#include "skip_list_common.hpp" + +#include "utils/skip_list.hpp" + +DEFINE_int32(max_element, 20000, "Maximum element in the intial list"); +DEFINE_int32(max_range, 2000000, "Maximum range used for the test"); + +int main(int argc, char **argv) { + gflags::ParseCommandLineFlags(&argc, &argv, true); + google::InitGoogleLogging(argv[0]); + + utils::SkipList list; + + { + auto acc = list.access(); + for (uint64_t i = 0; i <= FLAGS_max_element; ++i) { + CHECK(acc.insert(i).second); + } + uint64_t val = 0; + for (auto item : acc) { + CHECK(item == val); + ++val; + } + CHECK(val == FLAGS_max_element + 1); + } + + RunConcurrentTest([&list](auto *run, auto *stats) { + std::mt19937 generator(std::random_device{}()); + std::uniform_int_distribution distribution(0, 9); + std::mt19937 i_generator(std::random_device{}()); + std::uniform_int_distribution i_distribution(0, FLAGS_max_range); + while (run->load(std::memory_order_relaxed)) { + auto value = distribution(generator); + auto accessor = list.access(); + auto item = i_distribution(i_generator); + switch (value) { + case 0: + stats->succ[OP_CONTAINS] += + static_cast(accessor.contains(item)); + break; + case 1: + case 2: + case 3: + case 4: + case 5: + stats->succ[OP_FIND] += + static_cast(accessor.find(item) != accessor.end()); + break; + case 6: + case 7: + case 8: + stats->succ[OP_INSERT] += + static_cast(accessor.insert(item).second); + break; + case 9: + stats->succ[OP_REMOVE] += + static_cast(accessor.remove(item)); + break; + default: + std::terminate(); + } + ++stats->total; + } + }); + + return 0; +} diff --git a/tests/benchmark/skip_list_same_item.cpp b/tests/benchmark/skip_list_same_item.cpp new file mode 100644 index 000000000..d5191ffb9 --- /dev/null +++ b/tests/benchmark/skip_list_same_item.cpp @@ -0,0 +1,41 @@ +#include "skip_list_common.hpp" + +#include "utils/skip_list.hpp" + +int main(int argc, char **argv) { + gflags::ParseCommandLineFlags(&argc, &argv, true); + google::InitGoogleLogging(argv[0]); + + utils::SkipList list; + + RunConcurrentTest([&list](auto *run, auto *stats) { + std::mt19937 generator(std::random_device{}()); + std::uniform_int_distribution distribution(0, 3); + while (run->load(std::memory_order_relaxed)) { + int value = distribution(generator); + auto accessor = list.access(); + switch (value) { + case 0: + stats->succ[OP_INSERT] += + static_cast(accessor.insert(5).second); + break; + case 1: + stats->succ[OP_CONTAINS] += + static_cast(accessor.contains(5)); + break; + case 2: + stats->succ[OP_REMOVE] += static_cast(accessor.remove(5)); + break; + case 3: + stats->succ[OP_FIND] += + static_cast(accessor.find(5) != accessor.end()); + break; + default: + std::terminate(); + } + ++stats->total; + } + }); + + return 0; +} diff --git a/tests/benchmark/skip_list_vs_stl.cpp b/tests/benchmark/skip_list_vs_stl.cpp new file mode 100644 index 000000000..07fbc2562 --- /dev/null +++ b/tests/benchmark/skip_list_vs_stl.cpp @@ -0,0 +1,316 @@ +#include +#include +#include +#include + +#include + +#include "utils/skip_list.hpp" +#include "utils/spin_lock.hpp" + +const int kThreadsNum = 8; +const uint64_t kMaxNum = 10000000; + +/////////////////////////////////////////////////////////////////////////////// +// utils::SkipList set Insert +/////////////////////////////////////////////////////////////////////////////// + +class SkipListSetInsertFixture : public benchmark::Fixture { + protected: + void SetUp(const benchmark::State &state) override { + if (state.thread_index == 0) { + list = {}; + } + } + + protected: + utils::SkipList list; +}; + +BENCHMARK_DEFINE_F(SkipListSetInsertFixture, Insert)(benchmark::State &state) { + std::mt19937 gen(state.thread_index); + std::uniform_int_distribution dist(0, kMaxNum); + uint64_t counter = 0; + while (state.KeepRunning()) { + auto acc = list.access(); + if (acc.insert(dist(gen)).second) { + ++counter; + } + } + state.SetItemsProcessed(counter); +} + +BENCHMARK_REGISTER_F(SkipListSetInsertFixture, Insert) + ->ThreadRange(1, kThreadsNum) + ->Unit(benchmark::kNanosecond) + ->UseRealTime(); + +/////////////////////////////////////////////////////////////////////////////// +// std::set Insert +/////////////////////////////////////////////////////////////////////////////// + +class StdSetInsertFixture : public benchmark::Fixture { + protected: + void SetUp(const benchmark::State &state) override { + if (state.thread_index == 0) { + container = {}; + } + } + + protected: + std::set container; + utils::SpinLock lock; +}; + +BENCHMARK_DEFINE_F(StdSetInsertFixture, Insert)(benchmark::State &state) { + std::mt19937 gen(state.thread_index); + std::uniform_int_distribution dist(0, kMaxNum); + uint64_t counter = 0; + while (state.KeepRunning()) { + std::lock_guard guard(lock); + if (container.insert(dist(gen)).second) { + ++counter; + } + } + state.SetItemsProcessed(counter); +} + +BENCHMARK_REGISTER_F(StdSetInsertFixture, Insert) + ->ThreadRange(1, kThreadsNum) + ->Unit(benchmark::kNanosecond) + ->UseRealTime(); + +/////////////////////////////////////////////////////////////////////////////// +// utils::SkipList set Find +/////////////////////////////////////////////////////////////////////////////// + +class SkipListSetFindFixture : public benchmark::Fixture { + protected: + void SetUp(const benchmark::State &state) override { + if (state.thread_index == 0 && list.size() == 0) { + auto acc = list.access(); + for (uint64_t i = 0; i < kMaxNum; ++i) { + acc.insert(i); + } + } + } + + protected: + utils::SkipList list; +}; + +BENCHMARK_DEFINE_F(SkipListSetFindFixture, Find)(benchmark::State &state) { + std::mt19937 gen(state.thread_index); + std::uniform_int_distribution dist(0, kMaxNum); + uint64_t counter = 0; + while (state.KeepRunning()) { + auto acc = list.access(); + if (acc.find(dist(gen)) != acc.end()) { + ++counter; + } + } + state.SetItemsProcessed(counter); +} + +BENCHMARK_REGISTER_F(SkipListSetFindFixture, Find) + ->ThreadRange(1, kThreadsNum) + ->Unit(benchmark::kNanosecond) + ->UseRealTime(); + +/////////////////////////////////////////////////////////////////////////////// +// std::set Find +/////////////////////////////////////////////////////////////////////////////// + +class StdSetFindFixture : public benchmark::Fixture { + protected: + void SetUp(const benchmark::State &state) override { + if (state.thread_index == 0 && container.size() == 0) { + for (uint64_t i = 0; i < kMaxNum; ++i) { + container.insert(i); + } + } + } + + protected: + std::set container; + utils::SpinLock lock; +}; + +BENCHMARK_DEFINE_F(StdSetFindFixture, Find)(benchmark::State &state) { + std::mt19937 gen(state.thread_index); + std::uniform_int_distribution dist(0, kMaxNum); + uint64_t counter = 0; + while (state.KeepRunning()) { + std::lock_guard guard(lock); + if (container.find(dist(gen)) != container.end()) { + ++counter; + } + } + state.SetItemsProcessed(counter); +} + +BENCHMARK_REGISTER_F(StdSetFindFixture, Find) + ->ThreadRange(1, kThreadsNum) + ->Unit(benchmark::kNanosecond) + ->UseRealTime(); + +/////////////////////////////////////////////////////////////////////////////// +// Map tests common +/////////////////////////////////////////////////////////////////////////////// + +struct MapObject { + uint64_t key; + uint64_t value; +}; + +bool operator==(const MapObject &a, const MapObject &b) { + return a.key == b.key; +} +bool operator<(const MapObject &a, const MapObject &b) { return a.key < b.key; } +bool operator==(const MapObject &a, uint64_t b) { return a.key == b; } +bool operator<(const MapObject &a, uint64_t b) { return a.key < b; } + +/////////////////////////////////////////////////////////////////////////////// +// utils::SkipList map Insert +/////////////////////////////////////////////////////////////////////////////// + +class SkipListMapInsertFixture : public benchmark::Fixture { + protected: + void SetUp(const benchmark::State &state) override { + if (state.thread_index == 0) { + list = {}; + } + } + + protected: + utils::SkipList list; +}; + +BENCHMARK_DEFINE_F(SkipListMapInsertFixture, Insert)(benchmark::State &state) { + std::mt19937 gen(state.thread_index); + std::uniform_int_distribution dist(0, kMaxNum); + uint64_t counter = 0; + while (state.KeepRunning()) { + auto acc = list.access(); + if (acc.insert({dist(gen), 0}).second) { + ++counter; + } + } + state.SetItemsProcessed(counter); +} + +BENCHMARK_REGISTER_F(SkipListMapInsertFixture, Insert) + ->ThreadRange(1, kThreadsNum) + ->Unit(benchmark::kNanosecond) + ->UseRealTime(); + +/////////////////////////////////////////////////////////////////////////////// +// std::map Insert +/////////////////////////////////////////////////////////////////////////////// + +class StdMapInsertFixture : public benchmark::Fixture { + protected: + void SetUp(const benchmark::State &state) override { + if (state.thread_index == 0) { + container = {}; + } + } + + protected: + std::map container; + utils::SpinLock lock; +}; + +BENCHMARK_DEFINE_F(StdMapInsertFixture, Insert)(benchmark::State &state) { + std::mt19937 gen(state.thread_index); + std::uniform_int_distribution dist(0, kMaxNum); + uint64_t counter = 0; + while (state.KeepRunning()) { + std::lock_guard guard(lock); + if (container.insert({dist(gen), 0}).second) { + ++counter; + } + } + state.SetItemsProcessed(counter); +} + +BENCHMARK_REGISTER_F(StdMapInsertFixture, Insert) + ->ThreadRange(1, kThreadsNum) + ->Unit(benchmark::kNanosecond) + ->UseRealTime(); + +/////////////////////////////////////////////////////////////////////////////// +// utils::SkipList map Find +/////////////////////////////////////////////////////////////////////////////// + +class SkipListMapFindFixture : public benchmark::Fixture { + protected: + void SetUp(const benchmark::State &state) override { + if (state.thread_index == 0 && list.size() == 0) { + auto acc = list.access(); + for (uint64_t i = 0; i < kMaxNum; ++i) { + acc.insert({i, 0}); + } + } + } + + protected: + utils::SkipList list; +}; + +BENCHMARK_DEFINE_F(SkipListMapFindFixture, Find)(benchmark::State &state) { + std::mt19937 gen(state.thread_index); + std::uniform_int_distribution dist(0, kMaxNum); + uint64_t counter = 0; + while (state.KeepRunning()) { + auto acc = list.access(); + if (acc.find(dist(gen)) != acc.end()) { + ++counter; + } + } + state.SetItemsProcessed(counter); +} + +BENCHMARK_REGISTER_F(SkipListMapFindFixture, Find) + ->ThreadRange(1, kThreadsNum) + ->Unit(benchmark::kNanosecond) + ->UseRealTime(); + +/////////////////////////////////////////////////////////////////////////////// +// std::map Find +/////////////////////////////////////////////////////////////////////////////// + +class StdMapFindFixture : public benchmark::Fixture { + protected: + void SetUp(const benchmark::State &state) override { + if (state.thread_index == 0 && container.size() == 0) { + for (uint64_t i = 0; i < kMaxNum; ++i) { + container.insert({i, 0}); + } + } + } + + protected: + std::map container; + utils::SpinLock lock; +}; + +BENCHMARK_DEFINE_F(StdMapFindFixture, Find)(benchmark::State &state) { + std::mt19937 gen(state.thread_index); + std::uniform_int_distribution dist(0, kMaxNum); + uint64_t counter = 0; + while (state.KeepRunning()) { + std::lock_guard guard(lock); + if (container.find(dist(gen)) != container.end()) { + ++counter; + } + } + state.SetItemsProcessed(counter); +} + +BENCHMARK_REGISTER_F(StdMapFindFixture, Find) + ->ThreadRange(1, kThreadsNum) + ->Unit(benchmark::kNanosecond) + ->UseRealTime(); + +BENCHMARK_MAIN(); diff --git a/tests/concurrent/CMakeLists.txt b/tests/concurrent/CMakeLists.txt index 58fd6fff5..4cc5b5ddd 100644 --- a/tests/concurrent/CMakeLists.txt +++ b/tests/concurrent/CMakeLists.txt @@ -44,6 +44,24 @@ target_link_libraries(${test_prefix}network_session_leak mg-single-node kvstore_ add_concurrent_test(push_queue.cpp) target_link_libraries(${test_prefix}push_queue mg-single-node kvstore_dummy_lib) +add_concurrent_test(stack.cpp) +target_link_libraries(${test_prefix}stack mg-utils) + +add_concurrent_test(skip_list_insert.cpp) +target_link_libraries(${test_prefix}skip_list_insert mg-utils) + +add_concurrent_test(skip_list_insert_competitive.cpp) +target_link_libraries(${test_prefix}skip_list_insert_competitive mg-utils) + +add_concurrent_test(skip_list_mixed.cpp) +target_link_libraries(${test_prefix}skip_list_mixed mg-utils) + +add_concurrent_test(skip_list_remove.cpp) +target_link_libraries(${test_prefix}skip_list_remove mg-utils) + +add_concurrent_test(skip_list_remove_competitive.cpp) +target_link_libraries(${test_prefix}skip_list_remove_competitive mg-utils) + add_concurrent_test(sl_hang.cpp) target_link_libraries(${test_prefix}sl_hang mg-single-node kvstore_dummy_lib) diff --git a/tests/concurrent/skip_list_insert.cpp b/tests/concurrent/skip_list_insert.cpp new file mode 100644 index 000000000..6c8bc7cce --- /dev/null +++ b/tests/concurrent/skip_list_insert.cpp @@ -0,0 +1,36 @@ +#include +#include + +#include + +#include "utils/skip_list.hpp" + +const int kNumThreads = 8; +const uint64_t kMaxNum = 10000000; + +int main() { + utils::SkipList list; + + std::vector threads; + for (int i = 0; i < kNumThreads; ++i) { + threads.push_back(std::thread([&list, i] { + for (uint64_t num = i * kMaxNum; num < (i + 1) * kMaxNum; ++num) { + auto acc = list.access(); + CHECK(acc.insert(num).second); + } + })); + } + for (int i = 0; i < kNumThreads; ++i) { + threads[i].join(); + } + + CHECK(list.size() == kMaxNum * kNumThreads); + for (uint64_t i = 0; i < kMaxNum * kNumThreads; ++i) { + auto acc = list.access(); + auto it = acc.find(i); + CHECK(it != acc.end()); + CHECK(*it == i); + } + + return 0; +} diff --git a/tests/concurrent/skip_list_insert_competitive.cpp b/tests/concurrent/skip_list_insert_competitive.cpp new file mode 100644 index 000000000..dd0f86f7c --- /dev/null +++ b/tests/concurrent/skip_list_insert_competitive.cpp @@ -0,0 +1,42 @@ +#include +#include +#include + +#include + +#include "utils/skip_list.hpp" + +const int kNumThreads = 8; +const uint64_t kMaxNum = 10000000; + +int main() { + utils::SkipList list; + + std::atomic success{0}; + + std::vector threads; + for (int i = 0; i < kNumThreads; ++i) { + threads.push_back(std::thread([&list, &success] { + for (uint64_t num = 0; num < kMaxNum; ++num) { + auto acc = list.access(); + if (acc.insert(num).second) { + success.fetch_add(1, std::memory_order_relaxed); + } + } + })); + } + for (int i = 0; i < kNumThreads; ++i) { + threads[i].join(); + } + CHECK(success == kMaxNum); + + CHECK(list.size() == kMaxNum); + for (uint64_t i = 0; i < kMaxNum; ++i) { + auto acc = list.access(); + auto it = acc.find(i); + CHECK(it != acc.end()); + CHECK(*it == i); + } + + return 0; +} diff --git a/tests/concurrent/skip_list_mixed.cpp b/tests/concurrent/skip_list_mixed.cpp new file mode 100644 index 000000000..ba13edfda --- /dev/null +++ b/tests/concurrent/skip_list_mixed.cpp @@ -0,0 +1,84 @@ +#include +#include +#include +#include +#include + +#include + +#include "utils/skip_list.hpp" + +// kNumThreadsRemove should be smaller than kNumThreadsInsert because there +// should be some leftover items in the list for the find threads. +const int kNumThreadsInsert = 5; +const int kNumThreadsRemove = 2; +const int kNumThreadsFind = 3; + +const uint64_t kMaxNum = 10000000; + +int main() { + utils::SkipList list; + + std::atomic run{true}, modify_done{false}; + + std::vector threads_modify, threads_find; + + for (int i = 0; i < kNumThreadsInsert; ++i) { + threads_modify.push_back(std::thread([&list, i] { + for (uint64_t num = i * kMaxNum; num < (i + 1) * kMaxNum; ++num) { + auto acc = list.access(); + CHECK(acc.insert(num).second); + } + })); + } + for (int i = 0; i < kNumThreadsRemove; ++i) { + threads_modify.push_back(std::thread([&list, i] { + for (uint64_t num = i * kMaxNum; num < (i + 1) * kMaxNum; ++num) { + auto acc = list.access(); + while (!acc.remove(num)) + ; + } + })); + } + + for (int i = 0; i < kNumThreadsFind; ++i) { + threads_find.push_back(std::thread([&list, &run, &modify_done, i] { + std::mt19937 gen(3137 + i); + std::uniform_int_distribution dist( + 0, kNumThreadsInsert * kMaxNum - 1); + while (run.load(std::memory_order_relaxed)) { + auto acc = list.access(); + auto num = dist(gen); + auto it = acc.find(num); + if (modify_done.load(std::memory_order_relaxed) && + num >= kNumThreadsRemove * kMaxNum) { + CHECK(it != acc.end()); + CHECK(*it == num); + } + } + })); + } + + for (int i = 0; i < threads_modify.size(); ++i) { + threads_modify[i].join(); + } + + modify_done.store(true, std::memory_order_relaxed); + std::this_thread::sleep_for(std::chrono::seconds(10)); + run.store(false, std::memory_order_relaxed); + + for (int i = 0; i < threads_find.size(); ++i) { + threads_find[i].join(); + } + + CHECK(list.size() == (kNumThreadsInsert - kNumThreadsRemove) * kMaxNum); + for (uint64_t i = kMaxNum * kNumThreadsRemove; + i < kMaxNum * kNumThreadsInsert; ++i) { + auto acc = list.access(); + auto it = acc.find(i); + CHECK(it != acc.end()); + CHECK(*it == i); + } + + return 0; +} diff --git a/tests/concurrent/skip_list_remove.cpp b/tests/concurrent/skip_list_remove.cpp new file mode 100644 index 000000000..007c4d28e --- /dev/null +++ b/tests/concurrent/skip_list_remove.cpp @@ -0,0 +1,43 @@ +#include +#include + +#include + +#include "utils/skip_list.hpp" + +const int kNumThreads = 8; +const uint64_t kMaxNum = 10000000; + +int main() { + utils::SkipList list; + + for (int i = 0; i < kMaxNum * kNumThreads; ++i) { + auto acc = list.access(); + auto ret = acc.insert(i); + CHECK(ret.first != acc.end()); + CHECK(ret.second); + } + + std::vector threads; + for (int i = 0; i < kNumThreads; ++i) { + threads.push_back(std::thread([&list, i] { + for (uint64_t num = i * kMaxNum; num < (i + 1) * kMaxNum; ++num) { + auto acc = list.access(); + CHECK(acc.remove(num)); + } + })); + } + for (int i = 0; i < kNumThreads; ++i) { + threads[i].join(); + } + + CHECK(list.size() == 0); + uint64_t count = 0; + auto acc = list.access(); + for (auto it = acc.begin(); it != acc.end(); ++it) { + ++count; + } + CHECK(count == 0); + + return 0; +} diff --git a/tests/concurrent/skip_list_remove_competitive.cpp b/tests/concurrent/skip_list_remove_competitive.cpp new file mode 100644 index 000000000..98b6acf85 --- /dev/null +++ b/tests/concurrent/skip_list_remove_competitive.cpp @@ -0,0 +1,49 @@ +#include +#include +#include + +#include + +#include "utils/skip_list.hpp" + +const int kNumThreads = 8; +const uint64_t kMaxNum = 10000000; + +int main() { + utils::SkipList list; + + for (int i = 0; i < kMaxNum; ++i) { + auto acc = list.access(); + auto ret = acc.insert(i); + CHECK(ret.first != acc.end()); + CHECK(ret.second); + } + + std::atomic success{0}; + + std::vector threads; + for (int i = 0; i < kNumThreads; ++i) { + threads.push_back(std::thread([&list, &success, i] { + for (uint64_t num = 0; num < kMaxNum; ++num) { + auto acc = list.access(); + if (acc.remove(num)) { + success.fetch_add(1, std::memory_order_relaxed); + } + } + })); + } + for (int i = 0; i < kNumThreads; ++i) { + threads[i].join(); + } + CHECK(success == kMaxNum); + + CHECK(list.size() == 0); + uint64_t count = 0; + auto acc = list.access(); + for (auto it = acc.begin(); it != acc.end(); ++it) { + ++count; + } + CHECK(count == 0); + + return 0; +} diff --git a/tests/concurrent/stack.cpp b/tests/concurrent/stack.cpp new file mode 100644 index 000000000..8afc35dec --- /dev/null +++ b/tests/concurrent/stack.cpp @@ -0,0 +1,66 @@ +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "utils/stack.hpp" +#include "utils/timer.hpp" + +const int kNumThreads = 4; + +DEFINE_int32(max_value, 100000000, "Maximum value that should be inserted"); + +int main(int argc, char **argv) { + gflags::ParseCommandLineFlags(&argc, &argv, true); + google::InitGoogleLogging(argv[0]); + + utils::Stack stack; + + std::vector threads; + utils::Timer timer; + for (int i = 0; i < kNumThreads; ++i) { + threads.push_back(std::thread([&stack, i] { + for (uint64_t item = i; item < FLAGS_max_value; item += kNumThreads) { + stack.Push(item); + } + })); + } + + std::atomic run{true}; + std::thread verify([&stack, &run] { + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + std::vector found; + found.resize(FLAGS_max_value); + std::experimental::optional item; + while (run || (item = stack.Pop())) { + if (item) { + CHECK(*item < FLAGS_max_value); + found[*item] = true; + } + } + CHECK(!stack.Pop()); + for (uint64_t i = 0; i < FLAGS_max_value; ++i) { + CHECK(found[i]); + } + }); + + for (int i = 0; i < kNumThreads; ++i) { + threads[i].join(); + } + + auto elapsed = timer.Elapsed().count(); + + run.store(false); + verify.join(); + + std::cout << "Duration: " << elapsed << std::endl; + std::cout << "Throughput: " << FLAGS_max_value / elapsed << std::endl; + + return 0; +} diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index a51854c30..383615d21 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -206,6 +206,9 @@ target_link_libraries(${test_prefix}record_edge_vertex_accessor mg-single-node k add_unit_test(serialization.cpp) target_link_libraries(${test_prefix}serialization mg-distributed kvstore_dummy_lib) +add_unit_test(skip_list.cpp) +target_link_libraries(${test_prefix}skip_list mg-utils) + add_unit_test(skiplist_access.cpp) target_link_libraries(${test_prefix}skiplist_access mg-single-node kvstore_dummy_lib) diff --git a/tests/unit/skip_list.cpp b/tests/unit/skip_list.cpp new file mode 100644 index 000000000..6fe356eb8 --- /dev/null +++ b/tests/unit/skip_list.cpp @@ -0,0 +1,391 @@ +#include + +#include + +#include +#include + +#include "utils/skip_list.hpp" + +TEST(SkipList, Int) { + utils::SkipList list; + { + auto acc = list.access(); + for (int64_t i = -10; i <= 10; ++i) { + auto res = acc.insert(i); + ASSERT_EQ(*res.first, i); + ASSERT_TRUE(res.second); + } + ASSERT_EQ(acc.size(), 21); + } + { + auto acc = list.access(); + for (int64_t i = -10; i <= 10; ++i) { + auto res = acc.insert(i); + ASSERT_EQ(*res.first, i); + ASSERT_FALSE(res.second); + } + ASSERT_EQ(acc.size(), 21); + } + { + auto acc = list.access(); + int64_t val = -10; + for (auto &item : acc) { + ASSERT_EQ(item, val); + ++val; + } + ASSERT_EQ(val, 11); + ASSERT_EQ(acc.size(), 21); + } + { + auto acc = list.access(); + for (int64_t i = -10; i <= 10; ++i) { + auto it = acc.find(i); + ASSERT_NE(it, acc.end()); + ASSERT_EQ(*it, i); + } + ASSERT_EQ(acc.size(), 21); + } + { + auto acc = list.access(); + for (int64_t i = -10; i <= 10; ++i) { + ASSERT_TRUE(acc.remove(i)); + } + ASSERT_EQ(acc.size(), 0); + } + { + auto acc = list.access(); + for (int64_t i = -10; i <= 10; ++i) { + ASSERT_FALSE(acc.remove(i)); + } + ASSERT_EQ(acc.size(), 0); + } +} + +TEST(SkipList, String) { + utils::SkipList list; + { + auto acc = list.access(); + for (int64_t i = -10; i <= 10; ++i) { + std::string str(fmt::format("str{}", i)); + auto res = acc.insert(str); + ASSERT_EQ(*res.first, str); + ASSERT_TRUE(res.second); + ASSERT_NE(str, ""); + } + ASSERT_EQ(acc.size(), 21); + } + { + auto acc = list.access(); + for (int64_t i = -10; i <= 10; ++i) { + std::string str(fmt::format("str{}", i)); + auto res = acc.insert(str); + ASSERT_EQ(*res.first, str); + ASSERT_FALSE(res.second); + ASSERT_NE(str, ""); + } + ASSERT_EQ(acc.size(), 21); + } + { + auto acc = list.access(); + int64_t pos = 0; + std::vector order{-1, -10, -2, -3, -4, -5, -6, -7, -8, -9, 0, + 1, 10, 2, 3, 4, 5, 6, 7, 8, 9}; + for (auto &item : acc) { + std::string str(fmt::format("str{}", order[pos])); + ASSERT_EQ(item, str); + ++pos; + } + ASSERT_EQ(pos, 21); + ASSERT_EQ(acc.size(), 21); + } + { + auto acc = list.access(); + for (int64_t i = -10; i <= 10; ++i) { + std::string str(fmt::format("str{}", i)); + auto it = acc.find(str); + ASSERT_NE(it, acc.end()); + ASSERT_EQ(*it, str); + } + ASSERT_EQ(acc.size(), 21); + } + { + auto acc = list.access(); + for (int64_t i = -10; i <= 10; ++i) { + std::string str(fmt::format("str{}", i)); + ASSERT_TRUE(acc.remove(str)); + } + ASSERT_EQ(acc.size(), 0); + } + { + auto acc = list.access(); + for (int64_t i = -10; i <= 10; ++i) { + std::string str(fmt::format("str{}", i)); + ASSERT_FALSE(acc.remove(str)); + } + ASSERT_EQ(acc.size(), 0); + } +} + +TEST(SkipList, StringMove) { + utils::SkipList list; + { + auto acc = list.access(); + for (int64_t i = -10; i <= 10; ++i) { + std::string str(fmt::format("str{}", i)); + std::string copy(str); + auto res = acc.insert(std::move(str)); + ASSERT_EQ(str, ""); + ASSERT_EQ(*res.first, copy); + ASSERT_TRUE(res.second); + } + ASSERT_EQ(acc.size(), 21); + } + { + auto acc = list.access(); + for (int64_t i = -10; i <= 10; ++i) { + std::string str(fmt::format("str{}", i)); + auto res = acc.insert(str); + ASSERT_EQ(*res.first, str); + ASSERT_FALSE(res.second); + ASSERT_NE(str, ""); + } + ASSERT_EQ(acc.size(), 21); + } +} + +TEST(SkipList, Basic) { + utils::SkipList list; + + auto acc = list.access(); + + auto insert_item = [&acc](auto item, bool inserted) { + auto ret = acc.insert(item); + ASSERT_NE(ret.first, acc.end()); + ASSERT_EQ(*ret.first, item); + ASSERT_EQ(ret.second, inserted); + }; + + auto find_item = [&acc](auto item, bool found) { + auto ret = acc.find(item); + if (found) { + ASSERT_NE(ret, acc.end()); + ASSERT_EQ(*ret, item); + } else { + ASSERT_EQ(ret, acc.end()); + } + }; + + ASSERT_FALSE(acc.contains(5)); + insert_item(5, true); + insert_item(1, true); + insert_item(2, true); + insert_item(3, true); + insert_item(4, true); + insert_item(5, false); + find_item(5, true); + find_item(6, false); + ASSERT_TRUE(acc.remove(5)); + ASSERT_FALSE(acc.remove(5)); + ASSERT_FALSE(acc.remove(6)); + ASSERT_EQ(acc.size(), 4); +} + +struct OnlyCopyable { + OnlyCopyable() = default; + OnlyCopyable(OnlyCopyable &&) = delete; + OnlyCopyable(const OnlyCopyable &) = default; + OnlyCopyable &operator=(OnlyCopyable &&) = delete; + OnlyCopyable &operator=(const OnlyCopyable &) = default; + uint64_t value; +}; + +bool operator==(const OnlyCopyable &a, const OnlyCopyable &b) { + return a.value == b.value; +} +bool operator<(const OnlyCopyable &a, const OnlyCopyable &b) { + return a.value < b.value; +} + +TEST(SkipList, OnlyCopyable) { + utils::SkipList list; + std::vector vec{{1}, {2}, {3}, {4}, {5}}; + auto acc = list.access(); + auto ret = acc.insert(vec[1]); + ASSERT_NE(ret.first, acc.end()); + ASSERT_EQ(*ret.first, OnlyCopyable{2}); + ASSERT_TRUE(ret.second); +} + +struct OnlyMoveable { + OnlyMoveable() = default; + OnlyMoveable(uint64_t val) : value(val) {} + OnlyMoveable(OnlyMoveable &&) = default; + OnlyMoveable(const OnlyMoveable &) = delete; + OnlyMoveable &operator=(OnlyMoveable &&) = default; + OnlyMoveable &operator=(const OnlyMoveable &) = delete; + uint64_t value; +}; + +bool operator==(const OnlyMoveable &a, const OnlyMoveable &b) { + return a.value == b.value; +} +bool operator<(const OnlyMoveable &a, const OnlyMoveable &b) { + return a.value < b.value; +} + +TEST(SkipList, OnlyMoveable) { + utils::SkipList list; + std::vector vec; + vec.push_back({1}); + vec.push_back({2}); + auto acc = list.access(); + auto ret = acc.insert(std::move(vec[1])); + ASSERT_NE(ret.first, acc.end()); + ASSERT_EQ(*ret.first, OnlyMoveable{2}); + ASSERT_TRUE(ret.second); +} + +TEST(SkipList, Const) { + utils::SkipList list; + + auto func = [](const utils::SkipList &lst) { + auto acc = lst.access(); + return acc.find(5); + }; + + auto acc = list.access(); + + CHECK(func(list) == acc.end()); +} + +struct MapObject { + uint64_t key; + std::string value; +}; + +bool operator==(const MapObject &a, const MapObject &b) { + return a.key == b.key; +} +bool operator<(const MapObject &a, const MapObject &b) { return a.key < b.key; } + +bool operator==(const MapObject &a, const uint64_t &b) { return a.key == b; } +bool operator<(const MapObject &a, const uint64_t &b) { return a.key < b; } + +TEST(SkipList, MapExample) { + utils::SkipList list; + { + auto accessor = list.access(); + + // Inserts an object into the list. + ASSERT_TRUE(accessor.insert(MapObject{5, "hello world"}).second); + + // This operation will return an iterator that isn't equal to + // `accessor.end()`. This is because the comparison operators only use + // the key field for comparison, the value field is ignored. + ASSERT_NE(accessor.find(MapObject{5, "this probably isn't desired"}), + accessor.end()); + + // This will also succeed in removing the object. + ASSERT_TRUE(accessor.remove(MapObject{5, "not good"})); + } + + { + auto accessor = list.access(); + + // Inserts an object into the list. + ASSERT_TRUE(accessor.insert({5, "hello world"}).second); + + // This successfully finds the inserted object. + ASSERT_NE(accessor.find(5), accessor.end()); + + // This successfully removes the inserted object. + ASSERT_TRUE(accessor.remove(5)); + } +} + +TEST(SkipList, Move) { + utils::SkipList list; + + { + auto acc = list.access(); + for (int64_t i = -1000; i <= 1000; ++i) { + acc.insert(i); + } + ASSERT_EQ(acc.size(), 2001); + } + + { + auto acc = list.access(); + int64_t val = -1000; + for (auto &item : acc) { + ASSERT_EQ(item, val); + ++val; + } + ASSERT_EQ(val, 1001); + ASSERT_EQ(acc.size(), 2001); + } + + utils::SkipList moved(std::move(list)); + + { + auto acc = moved.access(); + int64_t val = -1000; + for (auto &item : acc) { + ASSERT_EQ(item, val); + ++val; + } + ASSERT_EQ(val, 1001); + ASSERT_EQ(acc.size(), 2001); + } + + { + auto acc = list.access(); + ASSERT_DEATH(acc.insert(5), ""); + } +} + +struct Inception { + uint64_t id; + utils::SkipList data; +}; +bool operator==(const Inception &a, const Inception &b) { return a.id == b.id; } +bool operator<(const Inception &a, const Inception &b) { return a.id < b.id; } +bool operator==(const Inception &a, const uint64_t &b) { return a.id == b; } +bool operator<(const Inception &a, const uint64_t &b) { return a.id < b; } + +TEST(SkipList, Inception) { + utils::SkipList list; + + { + for (uint64_t i = 0; i < 5; ++i) { + utils::SkipList inner; + auto acc_inner = inner.access(); + for (uint64_t j = 0; j < 100; ++j) { + acc_inner.insert(j + 1000 * i); + } + ASSERT_EQ(acc_inner.size(), 100); + auto acc = list.access(); + acc.insert(Inception{i, std::move(inner)}); + auto dead = inner.access(); + ASSERT_DEATH(dead.insert(5), ""); + } + } + + { + ASSERT_EQ(list.size(), 5); + for (uint64_t i = 0; i < 5; ++i) { + auto acc = list.access(); + auto it = acc.find(i); + ASSERT_NE(it, acc.end()); + ASSERT_EQ(it->id, i); + auto acc_inner = it->data.access(); + ASSERT_EQ(acc_inner.size(), 100); + for (uint64_t j = 0; j < 100; ++j) { + auto it_inner = acc_inner.find(j + 1000 * i); + ASSERT_NE(it_inner, acc_inner.end()); + ASSERT_EQ(*it_inner, j + 1000 * i); + } + } + } +}