From c943e27d51400d70494f9962d96a966dc842cb3b Mon Sep 17 00:00:00 2001 From: Max Kellermann Date: Fri, 11 Nov 2022 21:04:03 +0100 Subject: [PATCH] util/IntrusiveHashSet: new class Incomplete draft implementation, just enough methods for current needs. --- src/util/IntrusiveHashSet.hxx | 236 +++++++++++++++++++++++++++++ test/util/TestIntrusiveHashSet.cxx | 144 ++++++++++++++++++ test/util/meson.build | 1 + 3 files changed, 381 insertions(+) create mode 100644 src/util/IntrusiveHashSet.hxx create mode 100644 test/util/TestIntrusiveHashSet.cxx diff --git a/src/util/IntrusiveHashSet.hxx b/src/util/IntrusiveHashSet.hxx new file mode 100644 index 000000000..8ddeefbfd --- /dev/null +++ b/src/util/IntrusiveHashSet.hxx @@ -0,0 +1,236 @@ +/* + * Copyright 2022 Max Kellermann + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the + * distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * FOUNDATION OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include "IntrusiveList.hxx" + +#include // for std::all_of() +#include +#include // for std::accumulate() + +template +struct IntrusiveHashSetHook { + using SiblingsHook = IntrusiveListHook; + + SiblingsHook intrusive_hash_set_siblings; +}; + +/** + * Detect the hook type. + */ +template +struct IntrusiveHashSetHookDetection { + /* TODO can this be simplified somehow, without checking for + all possible enum values? */ + using type = std::conditional_t, U>, + IntrusiveHashSetHook, + std::conditional_t, U>, + IntrusiveHashSetHook, + std::conditional_t, U>, + IntrusiveHashSetHook, + void>>>; +}; + +/** + * For classes which embed #IntrusiveHashSetHook as base class. + */ +template +struct IntrusiveHashSetBaseHookTraits { + template + using Hook = typename IntrusiveHashSetHookDetection::type; + + using ListHookTraits = + IntrusiveListMemberHookTraits<&T::intrusive_hash_set_siblings>; + + static constexpr T *Cast(Hook *node) noexcept { + return static_cast(node); + } + + static constexpr auto &ToHook(T &t) noexcept { + return static_cast &>(t); + } +}; + +/** + * For classes which embed #IntrusiveListHook as member. + */ +template +struct IntrusiveHashSetMemberHookTraits { + using T = MemberPointerContainerType; + using _Hook = MemberPointerType; + + template + using Hook = _Hook; +}; + +/** + * A hash table implementation which stores pointers to items which + * have an embedded #IntrusiveHashSetHook. The actual table is + * embedded with a compile-time fixed size in this object. + */ +template, + bool constant_time_size=false> +class IntrusiveHashSet { + [[no_unique_address]] + OptionalCounter counter; + + [[no_unique_address]] + Hash hash; + + [[no_unique_address]] + Equal equal; + + struct SlotHookTraits { + template + using HashSetHook = typename HookTraits::template Hook; + + template + using ListHook = IntrusiveListMemberHookTraits<&HashSetHook::intrusive_hash_set_siblings>; + + template + using Hook = typename HashSetHook::SiblingsHook; + + static constexpr T *Cast(IntrusiveListNode *node) noexcept { + auto *hook = ListHook::Cast(node); + return HookTraits::Cast(hook); + } + + static constexpr auto &ToHook(T &t) noexcept { + auto &hook = HookTraits::ToHook(t); + return hook.intrusive_hash_set_siblings; + } + }; + + using Slot = IntrusiveList; + std::array table; + + using slot_iterator = typename Slot::iterator; + +public: + using value_type = T; + using reference = T &; + using const_reference = const T &; + using pointer = T *; + using const_pointer = const T *; + using size_type = std::size_t; + + [[nodiscard]] + IntrusiveHashSet() noexcept = default; + + [[nodiscard]] + constexpr bool empty() noexcept { + if constexpr (constant_time_size) + return size() == 0; + else + return std::all_of(table.begin(), table.end(), [](const auto &slot){ + return slot.empty(); + }); + } + + [[nodiscard]] + constexpr size_type size() noexcept { + if constexpr (constant_time_size) + return counter; + else + return std::accumulate(table.begin(), table.end(), size_type{}, [](std::size_t n, const auto &slot){ + return n + slot.size(); + }); + } + + constexpr void clear() noexcept { + for (auto &i : table) + i.clear(); + + counter.reset(); + } + + template + constexpr void clear_and_dispose(D &&disposer) noexcept { + for (auto &i : table) + i.clear_and_dispose(disposer); + + counter.reset(); + } + + [[nodiscard]] + static constexpr slot_iterator iterator_to(reference item) noexcept { + return Slot::iterator_to(item); + } + + [[nodiscard]] [[gnu::pure]] + constexpr std::pair insert_check(const auto &key) noexcept { + auto &slot = GetSlot(key); + for (auto &i : slot) + if (equal(key, i)) + return {slot.iterator_to(i), false}; + + return {slot.begin(), true}; + } + + constexpr void insert(slot_iterator slot, reference item) noexcept { + ++counter; + GetSlot(item).insert(slot, item); + } + + constexpr void insert(reference item) noexcept { + ++counter; + GetSlot(item).push_front(item); + } + + constexpr slot_iterator erase(slot_iterator i) noexcept { + --counter; + return GetSlot(*i).erase(i); + } + + [[nodiscard]] [[gnu::pure]] + constexpr slot_iterator find(const auto &key) noexcept { + auto &slot = GetSlot(key); + for (auto &i : slot) + if (equal(key, i)) + return slot.iterator_to(i); + + return end(); + } + + constexpr slot_iterator end() noexcept { + return table.front().end(); + } + +private: + template + [[gnu::pure]] + [[nodiscard]] + constexpr auto &GetSlot(K &&key) noexcept { + const auto h = hash(std::forward(key)); + return table[h % table_size]; + } +}; diff --git a/test/util/TestIntrusiveHashSet.cxx b/test/util/TestIntrusiveHashSet.cxx new file mode 100644 index 000000000..4e2c6770e --- /dev/null +++ b/test/util/TestIntrusiveHashSet.cxx @@ -0,0 +1,144 @@ +/* + * Copyright 2022 Max Kellermann + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the + * distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * FOUNDATION OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "util/IntrusiveHashSet.hxx" + +#include + +#include + +namespace { + +struct IntItem final : IntrusiveHashSetHook { + int value; + + IntItem(int _value) noexcept:value(_value) {} + + struct Hash { + constexpr std::size_t operator()(const IntItem &i) noexcept { + return i.value; + } + + constexpr std::size_t operator()(int i) noexcept { + return i; + } + }; + + struct Equal { + constexpr bool operator()(const IntItem &a, + const IntItem &b) noexcept { + return a.value == b.value; + } + }; +}; + +} // anonymous namespace + +TEST(IntrusiveHashSet, Basic) +{ + IntItem a{1}, b{2}, c{3}, d{4}, e{5}, f{1}; + + IntrusiveHashSet set; + + { + auto [position, inserted] = set.insert_check(2); + ASSERT_TRUE(inserted); + set.insert(position, b); + } + + ASSERT_FALSE(set.insert_check(2).second); + ASSERT_FALSE(set.insert_check(b).second); + + { + auto [position, inserted] = set.insert_check(a); + ASSERT_TRUE(inserted); + set.insert(position, a); + } + + set.insert(c); + + ASSERT_EQ(set.size(), 3); + + ASSERT_NE(set.find(c), set.end()); + ASSERT_EQ(set.find(c), set.iterator_to(c)); + ASSERT_NE(set.find(3), set.end()); + ASSERT_EQ(set.find(3), set.iterator_to(c)); + + ASSERT_EQ(set.find(4), set.end()); + ASSERT_EQ(set.find(d), set.end()); + + set.erase(set.iterator_to(c)); + + ASSERT_EQ(set.size(), 2); + ASSERT_EQ(set.find(3), set.end()); + ASSERT_EQ(set.find(c), set.end()); + + set.insert(c); + set.insert(d); + set.insert(e); + + ASSERT_EQ(set.size(), 5); + ASSERT_FALSE(set.insert_check(1).second); + ASSERT_EQ(set.insert_check(1).first, set.iterator_to(a)); + ASSERT_FALSE(set.insert_check(f).second); + ASSERT_EQ(set.insert_check(f).first, set.iterator_to(a)); + + ASSERT_EQ(set.find(1), set.iterator_to(a)); + ASSERT_EQ(set.find(2), set.iterator_to(b)); + ASSERT_EQ(set.find(3), set.iterator_to(c)); + ASSERT_EQ(set.find(4), set.iterator_to(d)); + ASSERT_EQ(set.find(5), set.iterator_to(e)); + + ASSERT_EQ(set.find(a), set.iterator_to(a)); + ASSERT_EQ(set.find(b), set.iterator_to(b)); + ASSERT_EQ(set.find(c), set.iterator_to(c)); + ASSERT_EQ(set.find(d), set.iterator_to(d)); + ASSERT_EQ(set.find(e), set.iterator_to(e)); + + set.erase(set.find(1)); + + { + auto [position, inserted] = set.insert_check(f); + ASSERT_TRUE(inserted); + set.insert(position, f); + } + + ASSERT_EQ(set.find(a), set.iterator_to(f)); + ASSERT_EQ(set.find(f), set.iterator_to(f)); + ASSERT_EQ(set.find(1), set.iterator_to(f)); + + set.clear_and_dispose([](auto *i){ i->value = -1; }); + + ASSERT_EQ(a.value, 1); + ASSERT_EQ(b.value, -1); + ASSERT_EQ(c.value, -1); + ASSERT_EQ(d.value, -1); + ASSERT_EQ(e.value, -1); + ASSERT_EQ(f.value, -1); +} diff --git a/test/util/meson.build b/test/util/meson.build index d69c2f3b3..2163af6ac 100644 --- a/test/util/meson.build +++ b/test/util/meson.build @@ -6,6 +6,7 @@ test( 'TestDivideString.cxx', 'TestException.cxx', 'TestIntrusiveForwardList.cxx', + 'TestIntrusiveHashSet.cxx', 'TestIntrusiveList.cxx', 'TestMimeType.cxx', 'TestSplitString.cxx',