385 lines
12 KiB
C++
385 lines
12 KiB
C++
//===- TrieRawHashMap.h -----------------------------------------*- C++ -*-===//
|
|
//
|
|
// 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
|
|
//
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
#ifndef LLVM_ADT_TRIERAWHASHMAP_H
|
|
#define LLVM_ADT_TRIERAWHASHMAP_H
|
|
|
|
#include "llvm/ADT/ArrayRef.h"
|
|
#include "llvm/Support/Compiler.h"
|
|
#include <atomic>
|
|
#include <optional>
|
|
|
|
namespace llvm {
|
|
|
|
class raw_ostream;
|
|
|
|
/// TrieRawHashMap - is a lock-free thread-safe trie that is can be used to
|
|
/// store/index data based on a hash value. It can be customized to work with
|
|
/// any hash algorithm or store any data.
|
|
///
|
|
/// Data structure:
|
|
/// Data node stored in the Trie contains both hash and data:
|
|
/// struct {
|
|
/// HashT Hash;
|
|
/// DataT Data;
|
|
/// };
|
|
///
|
|
/// Data is stored/indexed via a prefix tree, where each node in the tree can be
|
|
/// either the root, a sub-trie or a data node. Assuming a 4-bit hash and two
|
|
/// data objects {0001, A} and {0100, B}, it can be stored in a trie
|
|
/// (assuming Root has 2 bits, SubTrie has 1 bit):
|
|
/// +--------+
|
|
/// |Root[00]| -> {0001, A}
|
|
/// | [01]| -> {0100, B}
|
|
/// | [10]| (empty)
|
|
/// | [11]| (empty)
|
|
/// +--------+
|
|
///
|
|
/// Inserting a new object {0010, C} will result in:
|
|
/// +--------+ +----------+
|
|
/// |Root[00]| -> |SubTrie[0]| -> {0001, A}
|
|
/// | | | [1]| -> {0010, C}
|
|
/// | | +----------+
|
|
/// | [01]| -> {0100, B}
|
|
/// | [10]| (empty)
|
|
/// | [11]| (empty)
|
|
/// +--------+
|
|
/// Note object A is sunk down to a sub-trie during the insertion. All the
|
|
/// nodes are inserted through compare-exchange to ensure thread-safe and
|
|
/// lock-free.
|
|
///
|
|
/// To find an object in the trie, walk the tree with prefix of the hash until
|
|
/// the data node is found. Then the hash is compared with the hash stored in
|
|
/// the data node to see if the is the same object.
|
|
///
|
|
/// Hash collision is not allowed so it is recommended to use trie with a
|
|
/// "strong" hashing algorithm. A well-distributed hash can also result in
|
|
/// better performance and memory usage.
|
|
///
|
|
/// It currently does not support iteration and deletion.
|
|
|
|
/// Base class for a lock-free thread-safe hash-mapped trie.
|
|
class ThreadSafeTrieRawHashMapBase {
|
|
public:
|
|
static constexpr size_t TrieContentBaseSize = 4;
|
|
static constexpr size_t DefaultNumRootBits = 6;
|
|
static constexpr size_t DefaultNumSubtrieBits = 4;
|
|
|
|
private:
|
|
template <class T> struct AllocValueType {
|
|
char Base[TrieContentBaseSize];
|
|
alignas(T) char Content[sizeof(T)];
|
|
};
|
|
|
|
protected:
|
|
template <class T>
|
|
static constexpr size_t DefaultContentAllocSize = sizeof(AllocValueType<T>);
|
|
|
|
template <class T>
|
|
static constexpr size_t DefaultContentAllocAlign = alignof(AllocValueType<T>);
|
|
|
|
template <class T>
|
|
static constexpr size_t DefaultContentOffset =
|
|
offsetof(AllocValueType<T>, Content);
|
|
|
|
public:
|
|
static void *operator new(size_t Size) { return ::operator new(Size); }
|
|
void operator delete(void *Ptr) { ::operator delete(Ptr); }
|
|
|
|
#if !defined(NDEBUG) || defined(LLVM_ENABLE_DUMP)
|
|
LLVM_DUMP_METHOD void dump() const;
|
|
#endif
|
|
|
|
LLVM_ABI void print(raw_ostream &OS) const;
|
|
|
|
protected:
|
|
/// Result of a lookup. Suitable for an insertion hint. Maybe could be
|
|
/// expanded into an iterator of sorts, but likely not useful (visiting
|
|
/// everything in the trie should probably be done some way other than
|
|
/// through an iterator pattern).
|
|
class PointerBase {
|
|
protected:
|
|
void *get() const { return I == -2u ? P : nullptr; }
|
|
|
|
public:
|
|
PointerBase() noexcept = default;
|
|
|
|
private:
|
|
friend class ThreadSafeTrieRawHashMapBase;
|
|
explicit PointerBase(void *Content) : P(Content), I(-2u) {}
|
|
PointerBase(void *P, unsigned I, unsigned B) : P(P), I(I), B(B) {}
|
|
|
|
bool isHint() const { return I != -1u && I != -2u; }
|
|
|
|
void *P = nullptr;
|
|
unsigned I = -1u;
|
|
unsigned B = 0;
|
|
};
|
|
|
|
/// Find the stored content with hash.
|
|
LLVM_ABI PointerBase find(ArrayRef<uint8_t> Hash) const;
|
|
|
|
/// Insert and return the stored content.
|
|
LLVM_ABI PointerBase
|
|
insert(PointerBase Hint, ArrayRef<uint8_t> Hash,
|
|
function_ref<const uint8_t *(void *Mem, ArrayRef<uint8_t> Hash)>
|
|
Constructor);
|
|
|
|
ThreadSafeTrieRawHashMapBase() = delete;
|
|
|
|
LLVM_ABI ThreadSafeTrieRawHashMapBase(
|
|
size_t ContentAllocSize, size_t ContentAllocAlign, size_t ContentOffset,
|
|
std::optional<size_t> NumRootBits = std::nullopt,
|
|
std::optional<size_t> NumSubtrieBits = std::nullopt);
|
|
|
|
/// Destructor, which asserts if there's anything to do. Subclasses should
|
|
/// call \a destroyImpl().
|
|
///
|
|
/// \pre \a destroyImpl() was already called.
|
|
LLVM_ABI ~ThreadSafeTrieRawHashMapBase();
|
|
LLVM_ABI void destroyImpl(function_ref<void(void *ValueMem)> Destructor);
|
|
|
|
LLVM_ABI ThreadSafeTrieRawHashMapBase(ThreadSafeTrieRawHashMapBase &&RHS);
|
|
|
|
// Move assignment is not supported as it is not thread-safe.
|
|
ThreadSafeTrieRawHashMapBase &
|
|
operator=(ThreadSafeTrieRawHashMapBase &&RHS) = delete;
|
|
|
|
// No copy.
|
|
ThreadSafeTrieRawHashMapBase(const ThreadSafeTrieRawHashMapBase &) = delete;
|
|
ThreadSafeTrieRawHashMapBase &
|
|
operator=(const ThreadSafeTrieRawHashMapBase &) = delete;
|
|
|
|
// Debug functions. Implementation details and not guaranteed to be
|
|
// thread-safe.
|
|
LLVM_ABI PointerBase getRoot() const;
|
|
LLVM_ABI unsigned getStartBit(PointerBase P) const;
|
|
LLVM_ABI unsigned getNumBits(PointerBase P) const;
|
|
LLVM_ABI unsigned getNumSlotUsed(PointerBase P) const;
|
|
LLVM_ABI std::string getTriePrefixAsString(PointerBase P) const;
|
|
LLVM_ABI unsigned getNumTries() const;
|
|
// Visit next trie in the allocation chain.
|
|
LLVM_ABI PointerBase getNextTrie(PointerBase P) const;
|
|
|
|
private:
|
|
friend class TrieRawHashMapTestHelper;
|
|
const unsigned short ContentAllocSize;
|
|
const unsigned short ContentAllocAlign;
|
|
const unsigned short ContentOffset;
|
|
unsigned short NumRootBits;
|
|
unsigned short NumSubtrieBits;
|
|
class ImplType;
|
|
// ImplPtr is owned by ThreadSafeTrieRawHashMapBase and needs to be freed in
|
|
// destroyImpl.
|
|
std::atomic<ImplType *> ImplPtr;
|
|
ImplType &getOrCreateImpl();
|
|
ImplType *getImpl() const;
|
|
};
|
|
|
|
/// Lock-free thread-safe hash-mapped trie.
|
|
template <class T, size_t NumHashBytes>
|
|
class ThreadSafeTrieRawHashMap : public ThreadSafeTrieRawHashMapBase {
|
|
public:
|
|
using HashT = std::array<uint8_t, NumHashBytes>;
|
|
|
|
class LazyValueConstructor;
|
|
struct value_type {
|
|
const HashT Hash;
|
|
T Data;
|
|
|
|
value_type(value_type &&) = default;
|
|
value_type(const value_type &) = default;
|
|
|
|
value_type(ArrayRef<uint8_t> Hash, const T &Data)
|
|
: Hash(makeHash(Hash)), Data(Data) {}
|
|
value_type(ArrayRef<uint8_t> Hash, T &&Data)
|
|
: Hash(makeHash(Hash)), Data(std::move(Data)) {}
|
|
|
|
private:
|
|
friend class LazyValueConstructor;
|
|
|
|
struct EmplaceTag {};
|
|
template <class... ArgsT>
|
|
value_type(ArrayRef<uint8_t> Hash, EmplaceTag, ArgsT &&...Args)
|
|
: Hash(makeHash(Hash)), Data(std::forward<ArgsT>(Args)...) {}
|
|
|
|
static HashT makeHash(ArrayRef<uint8_t> HashRef) {
|
|
HashT Hash;
|
|
std::copy(HashRef.begin(), HashRef.end(), Hash.data());
|
|
return Hash;
|
|
}
|
|
};
|
|
|
|
using ThreadSafeTrieRawHashMapBase::operator delete;
|
|
using HashType = HashT;
|
|
|
|
#if !defined(NDEBUG) || defined(LLVM_ENABLE_DUMP)
|
|
using ThreadSafeTrieRawHashMapBase::dump;
|
|
#endif
|
|
|
|
using ThreadSafeTrieRawHashMapBase::print;
|
|
|
|
private:
|
|
template <class ValueT> class PointerImpl : PointerBase {
|
|
friend class ThreadSafeTrieRawHashMap;
|
|
|
|
ValueT *get() const {
|
|
return reinterpret_cast<ValueT *>(PointerBase::get());
|
|
}
|
|
|
|
public:
|
|
ValueT &operator*() const {
|
|
assert(get());
|
|
return *get();
|
|
}
|
|
ValueT *operator->() const {
|
|
assert(get());
|
|
return get();
|
|
}
|
|
explicit operator bool() const { return get(); }
|
|
|
|
PointerImpl() = default;
|
|
|
|
protected:
|
|
PointerImpl(PointerBase Result) : PointerBase(Result) {}
|
|
};
|
|
|
|
public:
|
|
class pointer;
|
|
class const_pointer;
|
|
class pointer : public PointerImpl<value_type> {
|
|
friend class ThreadSafeTrieRawHashMap;
|
|
friend class const_pointer;
|
|
|
|
public:
|
|
pointer() = default;
|
|
|
|
private:
|
|
pointer(PointerBase Result) : pointer::PointerImpl(Result) {}
|
|
};
|
|
|
|
class const_pointer : public PointerImpl<const value_type> {
|
|
friend class ThreadSafeTrieRawHashMap;
|
|
|
|
public:
|
|
const_pointer() = default;
|
|
const_pointer(const pointer &P) : const_pointer::PointerImpl(P) {}
|
|
|
|
private:
|
|
const_pointer(PointerBase Result) : const_pointer::PointerImpl(Result) {}
|
|
};
|
|
|
|
class LazyValueConstructor {
|
|
public:
|
|
value_type &operator()(T &&RHS) {
|
|
assert(Mem && "Constructor already called, or moved away");
|
|
return assign(::new (Mem) value_type(Hash, std::move(RHS)));
|
|
}
|
|
value_type &operator()(const T &RHS) {
|
|
assert(Mem && "Constructor already called, or moved away");
|
|
return assign(::new (Mem) value_type(Hash, RHS));
|
|
}
|
|
template <class... ArgsT> value_type &emplace(ArgsT &&...Args) {
|
|
assert(Mem && "Constructor already called, or moved away");
|
|
return assign(::new (Mem)
|
|
value_type(Hash, typename value_type::EmplaceTag{},
|
|
std::forward<ArgsT>(Args)...));
|
|
}
|
|
|
|
LazyValueConstructor(LazyValueConstructor &&RHS)
|
|
: Mem(RHS.Mem), Result(RHS.Result), Hash(RHS.Hash) {
|
|
RHS.Mem = nullptr; // Moved away, cannot call.
|
|
}
|
|
~LazyValueConstructor() { assert(!Mem && "Constructor never called!"); }
|
|
|
|
private:
|
|
value_type &assign(value_type *V) {
|
|
Mem = nullptr;
|
|
Result = V;
|
|
return *V;
|
|
}
|
|
friend class ThreadSafeTrieRawHashMap;
|
|
LazyValueConstructor() = delete;
|
|
LazyValueConstructor(void *Mem, value_type *&Result, ArrayRef<uint8_t> Hash)
|
|
: Mem(Mem), Result(Result), Hash(Hash) {
|
|
assert(Hash.size() == sizeof(HashT) && "Invalid hash");
|
|
assert(Mem && "Invalid memory for construction");
|
|
}
|
|
void *Mem;
|
|
value_type *&Result;
|
|
ArrayRef<uint8_t> Hash;
|
|
};
|
|
|
|
/// Insert with a hint. Default-constructed hint will work, but it's
|
|
/// recommended to start with a lookup to avoid overhead in object creation
|
|
/// if it already exists.
|
|
pointer insertLazy(const_pointer Hint, ArrayRef<uint8_t> Hash,
|
|
function_ref<void(LazyValueConstructor)> OnConstruct) {
|
|
return pointer(ThreadSafeTrieRawHashMapBase::insert(
|
|
Hint, Hash, [&](void *Mem, ArrayRef<uint8_t> Hash) {
|
|
value_type *Result = nullptr;
|
|
OnConstruct(LazyValueConstructor(Mem, Result, Hash));
|
|
return Result->Hash.data();
|
|
}));
|
|
}
|
|
|
|
pointer insertLazy(ArrayRef<uint8_t> Hash,
|
|
function_ref<void(LazyValueConstructor)> OnConstruct) {
|
|
return insertLazy(const_pointer(), Hash, OnConstruct);
|
|
}
|
|
|
|
pointer insert(const_pointer Hint, value_type &&HashedData) {
|
|
return insertLazy(Hint, HashedData.Hash, [&](LazyValueConstructor C) {
|
|
C(std::move(HashedData.Data));
|
|
});
|
|
}
|
|
|
|
pointer insert(const_pointer Hint, const value_type &HashedData) {
|
|
return insertLazy(Hint, HashedData.Hash,
|
|
[&](LazyValueConstructor C) { C(HashedData.Data); });
|
|
}
|
|
|
|
pointer find(ArrayRef<uint8_t> Hash) {
|
|
assert(Hash.size() == std::tuple_size<HashT>::value);
|
|
return ThreadSafeTrieRawHashMapBase::find(Hash);
|
|
}
|
|
|
|
const_pointer find(ArrayRef<uint8_t> Hash) const {
|
|
assert(Hash.size() == std::tuple_size<HashT>::value);
|
|
return ThreadSafeTrieRawHashMapBase::find(Hash);
|
|
}
|
|
|
|
ThreadSafeTrieRawHashMap(std::optional<size_t> NumRootBits = std::nullopt,
|
|
std::optional<size_t> NumSubtrieBits = std::nullopt)
|
|
: ThreadSafeTrieRawHashMapBase(DefaultContentAllocSize<value_type>,
|
|
DefaultContentAllocAlign<value_type>,
|
|
DefaultContentOffset<value_type>,
|
|
NumRootBits, NumSubtrieBits) {}
|
|
|
|
~ThreadSafeTrieRawHashMap() {
|
|
if constexpr (std::is_trivially_destructible<value_type>::value)
|
|
this->destroyImpl(nullptr);
|
|
else
|
|
this->destroyImpl(
|
|
[](void *P) { static_cast<value_type *>(P)->~value_type(); });
|
|
}
|
|
|
|
// Move constructor okay.
|
|
ThreadSafeTrieRawHashMap(ThreadSafeTrieRawHashMap &&) = default;
|
|
|
|
// No move assignment or any copy.
|
|
ThreadSafeTrieRawHashMap &operator=(ThreadSafeTrieRawHashMap &&) = delete;
|
|
ThreadSafeTrieRawHashMap(const ThreadSafeTrieRawHashMap &) = delete;
|
|
ThreadSafeTrieRawHashMap &
|
|
operator=(const ThreadSafeTrieRawHashMap &) = delete;
|
|
};
|
|
|
|
} // namespace llvm
|
|
|
|
#endif // LLVM_ADT_TRIERAWHASHMAP_H
|