| //===- TrieRawHashMap.cpp -------------------------------------------------===// |
| // |
| // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. |
| // See https://llvm.org/LICENSE.txt for license information. |
| // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception |
| // |
| //===----------------------------------------------------------------------===// |
| |
| #include "llvm/ADT/TrieRawHashMap.h" |
| #include "llvm/ADT/LazyAtomicPointer.h" |
| #include "llvm/ADT/StringExtras.h" |
| #include "llvm/ADT/TrieHashIndexGenerator.h" |
| #include "llvm/Support/Allocator.h" |
| #include "llvm/Support/Casting.h" |
| #include "llvm/Support/Debug.h" |
| #include "llvm/Support/ThreadSafeAllocator.h" |
| #include "llvm/Support/TrailingObjects.h" |
| #include "llvm/Support/raw_ostream.h" |
| #include <memory> |
| |
| using namespace llvm; |
| |
| namespace { |
| struct TrieNode { |
| const bool IsSubtrie = false; |
| |
| TrieNode(bool IsSubtrie) : IsSubtrie(IsSubtrie) {} |
| |
| static void *operator new(size_t Size) { return ::operator new(Size); } |
| void operator delete(void *Ptr) { ::operator delete(Ptr); } |
| }; |
| |
| struct TrieContent final : public TrieNode { |
| const uint8_t ContentOffset; |
| const uint8_t HashSize; |
| const uint8_t HashOffset; |
| |
| void *getValuePointer() const { |
| auto *Content = reinterpret_cast<const uint8_t *>(this) + ContentOffset; |
| return const_cast<uint8_t *>(Content); |
| } |
| |
| ArrayRef<uint8_t> getHash() const { |
| auto *Begin = reinterpret_cast<const uint8_t *>(this) + HashOffset; |
| return ArrayRef(Begin, Begin + HashSize); |
| } |
| |
| TrieContent(size_t ContentOffset, size_t HashSize, size_t HashOffset) |
| : TrieNode(/*IsSubtrie=*/false), ContentOffset(ContentOffset), |
| HashSize(HashSize), HashOffset(HashOffset) {} |
| |
| static bool classof(const TrieNode *TN) { return !TN->IsSubtrie; } |
| }; |
| |
| static_assert(sizeof(TrieContent) == |
| ThreadSafeTrieRawHashMapBase::TrieContentBaseSize, |
| "Check header assumption!"); |
| |
| class TrieSubtrie final |
| : public TrieNode, |
| private TrailingObjects<TrieSubtrie, LazyAtomicPointer<TrieNode>> { |
| public: |
| using Slot = LazyAtomicPointer<TrieNode>; |
| |
| Slot &get(size_t I) { return getTrailingObjects()[I]; } |
| TrieNode *load(size_t I) { return get(I).load(); } |
| |
| unsigned size() const { return Size; } |
| |
| TrieSubtrie * |
| sink(size_t I, TrieContent &Content, size_t NumSubtrieBits, size_t NewI, |
| function_ref<TrieSubtrie *(std::unique_ptr<TrieSubtrie>)> Saver); |
| |
| static std::unique_ptr<TrieSubtrie> create(size_t StartBit, size_t NumBits); |
| |
| explicit TrieSubtrie(size_t StartBit, size_t NumBits); |
| |
| static bool classof(const TrieNode *TN) { return TN->IsSubtrie; } |
| |
| static constexpr size_t sizeToAlloc(unsigned NumBits) { |
| assert(NumBits < 20 && "Tries should have fewer than ~1M slots"); |
| unsigned Count = 1u << NumBits; |
| return totalSizeToAlloc<LazyAtomicPointer<TrieNode>>(Count); |
| } |
| |
| private: |
| // FIXME: Use a bitset to speed up access: |
| // |
| // std::array<std::atomic<uint64_t>, NumSlots/64> IsSet; |
| // |
| // This will avoid needing to visit sparsely filled slots in |
| // \a ThreadSafeTrieRawHashMapBase::destroyImpl() when there's a non-trivial |
| // destructor. |
| // |
| // It would also greatly speed up iteration, if we add that some day, and |
| // allow get() to return one level sooner. |
| // |
| // This would be the algorithm for updating IsSet (after updating Slots): |
| // |
| // std::atomic<uint64_t> &Bits = IsSet[I.High]; |
| // const uint64_t NewBit = 1ULL << I.Low; |
| // uint64_t Old = 0; |
| // while (!Bits.compare_exchange_weak(Old, Old | NewBit)) |
| // ; |
| |
| // For debugging. |
| unsigned StartBit = 0; |
| unsigned NumBits = 0; |
| unsigned Size = 0; |
| friend class llvm::ThreadSafeTrieRawHashMapBase; |
| friend class TrailingObjects; |
| |
| public: |
| /// Linked list for ownership of tries. The pointer is owned by TrieSubtrie. |
| std::atomic<TrieSubtrie *> Next; |
| }; |
| } // end namespace |
| |
| std::unique_ptr<TrieSubtrie> TrieSubtrie::create(size_t StartBit, |
| size_t NumBits) { |
| void *Memory = ::operator new(sizeToAlloc(NumBits)); |
| TrieSubtrie *S = ::new (Memory) TrieSubtrie(StartBit, NumBits); |
| return std::unique_ptr<TrieSubtrie>(S); |
| } |
| |
| TrieSubtrie::TrieSubtrie(size_t StartBit, size_t NumBits) |
| : TrieNode(true), StartBit(StartBit), NumBits(NumBits), Size(1u << NumBits), |
| Next(nullptr) { |
| for (unsigned I = 0; I < Size; ++I) |
| new (&get(I)) Slot(nullptr); |
| |
| static_assert( |
| std::is_trivially_destructible<LazyAtomicPointer<TrieNode>>::value, |
| "Expected no work in destructor for TrieNode"); |
| } |
| |
| // Sink the nodes down sub-trie when the object being inserted collides with |
| // the index of existing object in the trie. In this case, a new sub-trie needs |
| // to be allocated to hold existing object. |
| TrieSubtrie *TrieSubtrie::sink( |
| size_t I, TrieContent &Content, size_t NumSubtrieBits, size_t NewI, |
| function_ref<TrieSubtrie *(std::unique_ptr<TrieSubtrie>)> Saver) { |
| // Create a new sub-trie that points to the existing object with the new |
| // index for the next level. |
| assert(NumSubtrieBits > 0); |
| std::unique_ptr<TrieSubtrie> S = create(StartBit + NumBits, NumSubtrieBits); |
| |
| assert(NewI < Size); |
| S->get(NewI).store(&Content); |
| |
| // Using compare_exchange to atomically add back the new sub-trie to the trie |
| // in the place of the exsiting object. |
| TrieNode *ExistingNode = &Content; |
| assert(I < Size); |
| if (get(I).compare_exchange_strong(ExistingNode, S.get())) |
| return Saver(std::move(S)); |
| |
| // Another thread created a subtrie already. Return it and let "S" be |
| // destructed. |
| return cast<TrieSubtrie>(ExistingNode); |
| } |
| |
| class ThreadSafeTrieRawHashMapBase::ImplType final |
| : private TrailingObjects<ThreadSafeTrieRawHashMapBase::ImplType, |
| TrieSubtrie> { |
| public: |
| static std::unique_ptr<ImplType> create(size_t StartBit, size_t NumBits) { |
| size_t Size = sizeof(ImplType) + TrieSubtrie::sizeToAlloc(NumBits); |
| void *Memory = ::operator new(Size); |
| ImplType *Impl = ::new (Memory) ImplType(StartBit, NumBits); |
| return std::unique_ptr<ImplType>(Impl); |
| } |
| |
| // Save the Subtrie into the ownship list of the trie structure in a |
| // thread-safe way. The ownership transfer is done by compare_exchange the |
| // pointer value inside the unique_ptr. |
| TrieSubtrie *save(std::unique_ptr<TrieSubtrie> S) { |
| assert(!S->Next && "Expected S to a freshly-constructed leaf"); |
| |
| TrieSubtrie *CurrentHead = nullptr; |
| // Add ownership of "S" to front of the list, so that Root -> S -> |
| // Root.Next. This works by repeatedly setting S->Next to a candidate value |
| // of Root.Next (initially nullptr), then setting Root.Next to S once the |
| // candidate matches reality. |
| while (!getRoot()->Next.compare_exchange_weak(CurrentHead, S.get())) |
| S->Next.exchange(CurrentHead); |
| |
| // Ownership transferred to subtrie successfully. Release the unique_ptr. |
| return S.release(); |
| } |
| |
| // Get the root which is the trailing object. |
| TrieSubtrie *getRoot() { return getTrailingObjects(); } |
| |
| static void *operator new(size_t Size) { return ::operator new(Size); } |
| void operator delete(void *Ptr) { ::operator delete(Ptr); } |
| |
| /// FIXME: This should take a function that allocates and constructs the |
| /// content lazily (taking the hash as a separate parameter), in case of |
| /// collision. |
| ThreadSafeAllocator<BumpPtrAllocator> ContentAlloc; |
| |
| private: |
| friend class TrailingObjects; |
| |
| ImplType(size_t StartBit, size_t NumBits) { |
| ::new (getRoot()) TrieSubtrie(StartBit, NumBits); |
| } |
| }; |
| |
| ThreadSafeTrieRawHashMapBase::ImplType & |
| ThreadSafeTrieRawHashMapBase::getOrCreateImpl() { |
| if (ImplType *Impl = ImplPtr.load()) |
| return *Impl; |
| |
| // Create a new ImplType and store it if another thread doesn't do so first. |
| // If another thread wins this one is destroyed locally. |
| std::unique_ptr<ImplType> Impl = ImplType::create(0, NumRootBits); |
| ImplType *ExistingImpl = nullptr; |
| |
| // If the ownership transferred succesfully, release unique_ptr and return |
| // the pointer to the new ImplType. |
| if (ImplPtr.compare_exchange_strong(ExistingImpl, Impl.get())) |
| return *Impl.release(); |
| |
| // Already created, return the existing ImplType. |
| return *ExistingImpl; |
| } |
| |
| ThreadSafeTrieRawHashMapBase::PointerBase |
| ThreadSafeTrieRawHashMapBase::find(ArrayRef<uint8_t> Hash) const { |
| assert(!Hash.empty() && "Uninitialized hash"); |
| |
| ImplType *Impl = ImplPtr.load(); |
| if (!Impl) |
| return PointerBase(); |
| |
| TrieSubtrie *S = Impl->getRoot(); |
| TrieHashIndexGenerator IndexGen{NumRootBits, NumSubtrieBits, Hash}; |
| size_t Index = IndexGen.next(); |
| while (Index != IndexGen.end()) { |
| // Try to set the content. |
| TrieNode *Existing = S->get(Index); |
| if (!Existing) |
| return PointerBase(S, Index, *IndexGen.StartBit); |
| |
| // Check for an exact match. |
| if (auto *ExistingContent = dyn_cast<TrieContent>(Existing)) |
| return ExistingContent->getHash() == Hash |
| ? PointerBase(ExistingContent->getValuePointer()) |
| : PointerBase(S, Index, *IndexGen.StartBit); |
| |
| Index = IndexGen.next(); |
| S = cast<TrieSubtrie>(Existing); |
| } |
| llvm_unreachable("failed to locate the node after consuming all hash bytes"); |
| } |
| |
| ThreadSafeTrieRawHashMapBase::PointerBase ThreadSafeTrieRawHashMapBase::insert( |
| PointerBase Hint, ArrayRef<uint8_t> Hash, |
| function_ref<const uint8_t *(void *Mem, ArrayRef<uint8_t> Hash)> |
| Constructor) { |
| assert(!Hash.empty() && "Uninitialized hash"); |
| |
| ImplType &Impl = getOrCreateImpl(); |
| TrieSubtrie *S = Impl.getRoot(); |
| TrieHashIndexGenerator IndexGen{NumRootBits, NumSubtrieBits, Hash}; |
| size_t Index; |
| if (Hint.isHint()) { |
| S = static_cast<TrieSubtrie *>(Hint.P); |
| Index = IndexGen.hint(Hint.I, Hint.B); |
| } else { |
| Index = IndexGen.next(); |
| } |
| |
| while (Index != IndexGen.end()) { |
| // Load the node from the slot, allocating and calling the constructor if |
| // the slot is empty. |
| bool Generated = false; |
| TrieNode &Existing = S->get(Index).loadOrGenerate([&]() { |
| Generated = true; |
| |
| // Construct the value itself at the tail. |
| uint8_t *Memory = reinterpret_cast<uint8_t *>( |
| Impl.ContentAlloc.Allocate(ContentAllocSize, ContentAllocAlign)); |
| const uint8_t *HashStorage = Constructor(Memory + ContentOffset, Hash); |
| |
| // Construct the TrieContent header, passing in the offset to the hash. |
| TrieContent *Content = ::new (Memory) |
| TrieContent(ContentOffset, Hash.size(), HashStorage - Memory); |
| assert(Hash == Content->getHash() && "Hash not properly initialized"); |
| return Content; |
| }); |
| // If we just generated it, return it! |
| if (Generated) |
| return PointerBase(cast<TrieContent>(Existing).getValuePointer()); |
| |
| if (auto *ST = dyn_cast<TrieSubtrie>(&Existing)) { |
| S = ST; |
| Index = IndexGen.next(); |
| continue; |
| } |
| |
| // Return the existing content if it's an exact match! |
| auto &ExistingContent = cast<TrieContent>(Existing); |
| if (ExistingContent.getHash() == Hash) |
| return PointerBase(ExistingContent.getValuePointer()); |
| |
| // Sink the existing content as long as the indexes match. |
| size_t NextIndex = IndexGen.next(); |
| while (NextIndex != IndexGen.end()) { |
| size_t NewIndexForExistingContent = |
| IndexGen.getCollidingBits(ExistingContent.getHash()); |
| S = S->sink(Index, ExistingContent, IndexGen.getNumBits(), |
| NewIndexForExistingContent, |
| [&Impl](std::unique_ptr<TrieSubtrie> S) { |
| return Impl.save(std::move(S)); |
| }); |
| Index = NextIndex; |
| |
| // Found the difference. |
| if (NextIndex != NewIndexForExistingContent) |
| break; |
| |
| NextIndex = IndexGen.next(); |
| } |
| } |
| llvm_unreachable("failed to insert the node after consuming all hash bytes"); |
| } |
| |
| ThreadSafeTrieRawHashMapBase::ThreadSafeTrieRawHashMapBase( |
| size_t ContentAllocSize, size_t ContentAllocAlign, size_t ContentOffset, |
| std::optional<size_t> NumRootBits, std::optional<size_t> NumSubtrieBits) |
| : ContentAllocSize(ContentAllocSize), ContentAllocAlign(ContentAllocAlign), |
| ContentOffset(ContentOffset), |
| NumRootBits(NumRootBits.value_or(DefaultNumRootBits)), |
| NumSubtrieBits(NumSubtrieBits.value_or(DefaultNumSubtrieBits)), |
| ImplPtr(nullptr) { |
| // Assertion checks for reasonable configuration. The settings below are not |
| // hard limits on most platforms, but a reasonable configuration should fall |
| // within those limits. |
| assert((!NumRootBits || *NumRootBits < 20) && |
| "Root should have fewer than ~1M slots"); |
| assert((!NumSubtrieBits || *NumSubtrieBits < 10) && |
| "Subtries should have fewer than ~1K slots"); |
| } |
| |
| ThreadSafeTrieRawHashMapBase::ThreadSafeTrieRawHashMapBase( |
| ThreadSafeTrieRawHashMapBase &&RHS) |
| : ContentAllocSize(RHS.ContentAllocSize), |
| ContentAllocAlign(RHS.ContentAllocAlign), |
| ContentOffset(RHS.ContentOffset), NumRootBits(RHS.NumRootBits), |
| NumSubtrieBits(RHS.NumSubtrieBits) { |
| // Steal the root from RHS. |
| ImplPtr = RHS.ImplPtr.exchange(nullptr); |
| } |
| |
| ThreadSafeTrieRawHashMapBase::~ThreadSafeTrieRawHashMapBase() { |
| assert(!ImplPtr.load() && "Expected subclass to call destroyImpl()"); |
| } |
| |
| void ThreadSafeTrieRawHashMapBase::destroyImpl( |
| function_ref<void(void *)> Destructor) { |
| std::unique_ptr<ImplType> Impl(ImplPtr.exchange(nullptr)); |
| if (!Impl) |
| return; |
| |
| // Destroy content nodes throughout trie. Avoid destroying any subtries since |
| // we need TrieNode::classof() to find the content nodes. |
| // |
| // FIXME: Once we have bitsets (see FIXME in TrieSubtrie class), use them |
| // facilitate sparse iteration here. |
| if (Destructor) |
| for (TrieSubtrie *Trie = Impl->getRoot(); Trie; Trie = Trie->Next.load()) |
| for (unsigned I = 0; I < Trie->size(); ++I) |
| if (auto *Content = dyn_cast_or_null<TrieContent>(Trie->load(I))) |
| Destructor(Content->getValuePointer()); |
| |
| // Destroy the subtries. Incidentally, this destroys them in the reverse order |
| // of saving. |
| TrieSubtrie *Trie = Impl->getRoot()->Next; |
| while (Trie) { |
| TrieSubtrie *Next = Trie->Next.exchange(nullptr); |
| delete Trie; |
| Trie = Next; |
| } |
| } |
| |
| ThreadSafeTrieRawHashMapBase::PointerBase |
| ThreadSafeTrieRawHashMapBase::getRoot() const { |
| ImplType *Impl = ImplPtr.load(); |
| if (!Impl) |
| return PointerBase(); |
| return PointerBase(Impl->getRoot()); |
| } |
| |
| unsigned ThreadSafeTrieRawHashMapBase::getStartBit( |
| ThreadSafeTrieRawHashMapBase::PointerBase P) const { |
| assert(!P.isHint() && "Not a valid trie"); |
| if (!P.P) |
| return 0; |
| if (auto *S = dyn_cast<TrieSubtrie>((TrieNode *)P.P)) |
| return S->StartBit; |
| return 0; |
| } |
| |
| unsigned ThreadSafeTrieRawHashMapBase::getNumBits( |
| ThreadSafeTrieRawHashMapBase::PointerBase P) const { |
| assert(!P.isHint() && "Not a valid trie"); |
| if (!P.P) |
| return 0; |
| if (auto *S = dyn_cast<TrieSubtrie>((TrieNode *)P.P)) |
| return S->NumBits; |
| return 0; |
| } |
| |
| unsigned ThreadSafeTrieRawHashMapBase::getNumSlotUsed( |
| ThreadSafeTrieRawHashMapBase::PointerBase P) const { |
| assert(!P.isHint() && "Not a valid trie"); |
| if (!P.P) |
| return 0; |
| auto *S = dyn_cast<TrieSubtrie>((TrieNode *)P.P); |
| if (!S) |
| return 0; |
| unsigned Num = 0; |
| for (unsigned I = 0, E = S->size(); I < E; ++I) |
| if (S->load(I)) |
| ++Num; |
| return Num; |
| } |
| |
| std::string ThreadSafeTrieRawHashMapBase::getTriePrefixAsString( |
| ThreadSafeTrieRawHashMapBase::PointerBase P) const { |
| assert(!P.isHint() && "Not a valid trie"); |
| if (!P.P) |
| return ""; |
| |
| auto *S = dyn_cast<TrieSubtrie>((TrieNode *)P.P); |
| if (!S || !S->IsSubtrie) |
| return ""; |
| |
| // Find a TrieContent node which has hash stored. Depth search following the |
| // first used slot until a TrieContent node is found. |
| TrieSubtrie *Current = S; |
| TrieContent *Node = nullptr; |
| while (Current) { |
| TrieSubtrie *Next = nullptr; |
| // Find first used slot in the trie. |
| for (unsigned I = 0, E = Current->size(); I < E; ++I) { |
| auto *S = Current->load(I); |
| if (!S) |
| continue; |
| |
| if (auto *Content = dyn_cast<TrieContent>(S)) |
| Node = Content; |
| else if (auto *Sub = dyn_cast<TrieSubtrie>(S)) |
| Next = Sub; |
| break; |
| } |
| |
| // Found the node. |
| if (Node) |
| break; |
| |
| // Continue to the next level if the node is not found. |
| Current = Next; |
| } |
| |
| assert(Node && "malformed trie, cannot find TrieContent on leaf node"); |
| // The prefix for the current trie is the first `StartBit` of the content |
| // stored underneath this subtrie. |
| std::string Str; |
| raw_string_ostream SS(Str); |
| |
| unsigned StartFullBytes = (S->StartBit + 1) / 8 - 1; |
| SS << toHex(toStringRef(Node->getHash()).take_front(StartFullBytes), |
| /*LowerCase=*/true); |
| |
| // For the part of the prefix that doesn't fill a byte, print raw bit values. |
| std::string Bits; |
| for (unsigned I = StartFullBytes * 8, E = S->StartBit; I < E; ++I) { |
| unsigned Index = I / 8; |
| unsigned Offset = 7 - I % 8; |
| Bits.push_back('0' + ((Node->getHash()[Index] >> Offset) & 1)); |
| } |
| |
| if (!Bits.empty()) |
| SS << "[" << Bits << "]"; |
| |
| return SS.str(); |
| } |
| |
| unsigned ThreadSafeTrieRawHashMapBase::getNumTries() const { |
| ImplType *Impl = ImplPtr.load(); |
| if (!Impl) |
| return 0; |
| unsigned Num = 0; |
| for (TrieSubtrie *Trie = Impl->getRoot(); Trie; Trie = Trie->Next.load()) |
| ++Num; |
| return Num; |
| } |
| |
| ThreadSafeTrieRawHashMapBase::PointerBase |
| ThreadSafeTrieRawHashMapBase::getNextTrie( |
| ThreadSafeTrieRawHashMapBase::PointerBase P) const { |
| assert(!P.isHint() && "Not a valid trie"); |
| if (!P.P) |
| return PointerBase(); |
| auto *S = dyn_cast<TrieSubtrie>((TrieNode *)P.P); |
| if (!S) |
| return PointerBase(); |
| if (auto *E = S->Next.load()) |
| return PointerBase(E); |
| return PointerBase(); |
| } |