/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
//--------------------------------------------------------------------------- // Overview //--------------------------------------------------------------------------- // // This file defines HashMap<Key, Value> and HashSet<T>, hash tables that are // fast and have a nice API. // // Both hash tables have two optional template parameters. // // - HashPolicy. This defines the operations for hashing and matching keys. The // default HashPolicy is appropriate when both of the following two // conditions are true. // // - The key type stored in the table (|Key| for |HashMap<Key, Value>|, |T| // for |HashSet<T>|) is an integer, pointer, UniquePtr, float, or double. // // - The type used for lookups (|Lookup|) is the same as the key type. This // is usually the case, but not always. // // There is also a |CStringHasher| policy for |char*| keys. If your keys // don't match any of the above cases, you must provide your own hash policy; // see the "Hash Policy" section below. // // - AllocPolicy. This defines how allocations are done by the table. // // - |MallocAllocPolicy| is the default and is usually appropriate; note that // operations (such as insertions) that might cause allocations are // fallible and must be checked for OOM. These checks are enforced by the // use of [[nodiscard]]. // // - |InfallibleAllocPolicy| is another possibility; it allows the // abovementioned OOM checks to be done with MOZ_ALWAYS_TRUE(). // // Note that entry storage allocation is lazy, and not done until the first // lookupForAdd(), put(), or putNew() is performed. // // See AllocPolicy.h for more details. // // Documentation on how to use HashMap and HashSet, including examples, is // present within those classes. Search for "class HashMap" and "class // HashSet". // // Both HashMap and HashSet are implemented on top of a third class, HashTable. // You only need to look at HashTable if you want to understand the // implementation. // // How does mozilla::HashTable (this file) compare with PLDHashTable (and its // subclasses, such as nsTHashtable)? // // - mozilla::HashTable is a lot faster, largely because it uses templates // throughout *and* inlines everything. PLDHashTable inlines operations much // less aggressively, and also uses "virtual ops" for operations like hashing // and matching entries that require function calls. // // - Correspondingly, mozilla::HashTable use is likely to increase executable // size much more than PLDHashTable. // // - mozilla::HashTable has a nicer API, with a proper HashSet vs. HashMap // distinction. // // - mozilla::HashTable requires more explicit OOM checking. As mentioned // above, the use of |InfallibleAllocPolicy| can simplify things. // // - mozilla::HashTable has a default capacity on creation of 32 and a minimum // capacity of 4. PLDHashTable has a default capacity on creation of 8 and a // minimum capacity of 8.
template <class, class = void> struct DefaultHasher;
template <class, class> class HashMapEntry;
namespace detail {
template <typename T> class HashTableEntry;
template <class T, class HashPolicy, class AllocPolicy> class HashTable;
} // namespace detail
// The "generation" of a hash table is an opaque value indicating the state of // modification of the hash table through its lifetime. If the generation of // a hash table compares equal at times T1 and T2, then lookups in the hash // table, pointers to (or into) hash table entries, etc. at time T1 are valid // at time T2. If the generation compares unequal, these computations are all // invalid and must be performed again to be used. // // Generations are meaningfully comparable only with respect to a single hash // table. It's always nonsensical to compare the generation of distinct hash // tables H1 and H2. using Generation = Opaque<uint64_t>;
// HashMap is a fast hash-based map from keys to values. // // Template parameter requirements: // - Key/Value: movable, destructible, assignable. // - HashPolicy: see the "Hash Policy" section below. // - AllocPolicy: see AllocPolicy.h. // // Note: // - HashMap is not reentrant: Key/Value/HashPolicy/AllocPolicy members // called by HashMap must not call back into the same HashMap object. // template <class Key, class Value, class HashPolicy = DefaultHasher<Key>, class AllocPolicy = MallocAllocPolicy> class HashMap { // -- Implementation details -----------------------------------------------
// HashMap is not copyable or assignable.
HashMap(const HashMap& hm) = delete;
HashMap& operator=(const HashMap& hm) = delete;
using TableEntry = HashMapEntry<Key, Value>;
struct MapHashPolicy : HashPolicy { using Base = HashPolicy; using KeyType = Key;
// Swap the contents of this hash map with another. void swap(HashMap& aOther) { mImpl.swap(aOther.mImpl); }
// -- Status and sizing ----------------------------------------------------
// The map's current generation.
Generation generation() const { return mImpl.generation(); }
// Is the map empty? bool empty() const { return mImpl.empty(); }
// Number of keys/values in the map.
uint32_t count() const { return mImpl.count(); }
// Number of key/value slots in the map. Note: resize will happen well before // count() == capacity().
uint32_t capacity() const { return mImpl.capacity(); }
// The size of the map's entry storage, in bytes. If the keys/values contain // pointers to other heap blocks, you must iterate over the map and measure // them separately; hence the "shallow" prefix.
size_t shallowSizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const { return mImpl.shallowSizeOfExcludingThis(aMallocSizeOf);
}
size_t shallowSizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const { return aMallocSizeOf(this) +
mImpl.shallowSizeOfExcludingThis(aMallocSizeOf);
}
// Attempt to minimize the capacity(). If the table is empty, this will free // the empty storage and upon regrowth it will be given the minimum capacity. void compact() { mImpl.compact(); }
// Attempt to reserve enough space to fit at least |aLen| elements. This is // total capacity, including elements already present. Does nothing if the // map already has sufficient capacity.
[[nodiscard]] bool reserve(uint32_t aLen) { return mImpl.reserve(aLen); }
// Does the map contain a key/value matching |aLookup|? bool has(const Lookup& aLookup) const { return mImpl.lookup(aLookup).found();
}
// Return a Ptr indicating whether a key/value matching |aLookup| is // present in the map. E.g.: // // using HM = HashMap<int,char>; // HM h; // if (HM::Ptr p = h.lookup(3)) { // assert(p->key() == 3); // char val = p->value(); // } // using Ptr = typename Impl::Ptr;
MOZ_ALWAYS_INLINE Ptr lookup(const Lookup& aLookup) const { return mImpl.lookup(aLookup);
}
// Like lookup(), but does not assert if two threads call it at the same // time. Only use this method when none of the threads will modify the map.
MOZ_ALWAYS_INLINE Ptr readonlyThreadsafeLookup(const Lookup& aLookup) const { return mImpl.readonlyThreadsafeLookup(aLookup);
}
// Overwrite existing value with |aValue|, or add it if not present. Returns // false on OOM. template <typename KeyInput, typename ValueInput>
[[nodiscard]] bool put(KeyInput&& aKey, ValueInput&& aValue) { return put(aKey, std::forward<KeyInput>(aKey),
std::forward<ValueInput>(aValue));
}
// Like put(), but slightly faster. Must only be used when the given key is // not already present. (In debug builds, assertions check this.) template <typename KeyInput, typename ValueInput>
[[nodiscard]] bool putNew(KeyInput&& aKey, ValueInput&& aValue) { return mImpl.putNew(aKey, std::forward<KeyInput>(aKey),
std::forward<ValueInput>(aValue));
}
// Like putNew(), but should be only used when the table is known to be big // enough for the insertion, and hashing cannot fail. Typically this is used // to populate an empty map with known-unique keys after reserving space with // reserve(), e.g. // // using HM = HashMap<int,char>; // HM h; // if (!h.reserve(3)) { // MOZ_CRASH("OOM"); // } // h.putNewInfallible(1, 'a'); // unique key // h.putNewInfallible(2, 'b'); // unique key // h.putNewInfallible(3, 'c'); // unique key // template <typename KeyInput, typename ValueInput> void putNewInfallible(KeyInput&& aKey, ValueInput&& aValue) {
mImpl.putNewInfallible(aKey, std::forward<KeyInput>(aKey),
std::forward<ValueInput>(aValue));
}
// Like |lookup(l)|, but on miss, |p = lookupForAdd(l)| allows efficient // insertion of Key |k| (where |HashPolicy::match(k,l) == true|) using // |add(p,k,v)|. After |add(p,k,v)|, |p| points to the new key/value. E.g.: // // using HM = HashMap<int,char>; // HM h; // HM::AddPtr p = h.lookupForAdd(3); // if (!p) { // if (!h.add(p, 3, 'a')) { // return false; // } // } // assert(p->key() == 3); // char val = p->value(); // // N.B. The caller must ensure that no mutating hash table operations occur // between a pair of lookupForAdd() and add() calls. To avoid looking up the // key a second time, the caller may use the more efficient relookupOrAdd() // method. This method reuses part of the hashing computation to more // efficiently insert the key if it has not been added. For example, a // mutation-handling version of the previous example: // // HM::AddPtr p = h.lookupForAdd(3); // if (!p) { // call_that_may_mutate_h(); // if (!h.relookupOrAdd(p, 3, 'a')) { // return false; // } // } // assert(p->key() == 3); // char val = p->value(); // using AddPtr = typename Impl::AddPtr;
MOZ_ALWAYS_INLINE AddPtr lookupForAdd(const Lookup& aLookup) { return mImpl.lookupForAdd(aLookup);
}
// Lookup and remove the key/value matching |aLookup|, if present. void remove(const Lookup& aLookup) { if (Ptr p = lookup(aLookup)) {
remove(p);
}
}
// Remove a previously found key/value (assuming aPtr.found()). The map must // not have been mutated in the interim. void remove(Ptr aPtr) { mImpl.remove(aPtr); }
// Remove all keys/values without changing the capacity. void clear() { mImpl.clear(); }
// Like clear() followed by compact(). void clearAndCompact() { mImpl.clearAndCompact(); }
// Infallibly rekey one entry, if necessary. Requires that template // parameters Key and HashPolicy::Lookup are the same type. void rekeyIfMoved(const Key& aOldKey, const Key& aNewKey) { if (aOldKey != aNewKey) {
rekeyAs(aOldKey, aNewKey, aNewKey);
}
}
// Infallibly rekey one entry if present, and return whether that happened. bool rekeyAs(const Lookup& aOldLookup, const Lookup& aNewLookup, const Key& aNewKey) { if (Ptr p = lookup(aOldLookup)) {
mImpl.rekeyAndMaybeRehash(p, aNewLookup, aNewKey); returntrue;
} returnfalse;
}
// |iter()| returns an Iterator: // // HashMap<int, char> h; // for (auto iter = h.iter(); !iter.done(); iter.next()) { // char c = iter.get().value(); // } // using Iterator = typename Impl::Iterator;
Iterator iter() const { return mImpl.iter(); }
// |modIter()| returns a ModIterator: // // HashMap<int, char> h; // for (auto iter = h.modIter(); !iter.done(); iter.next()) { // if (iter.get().value() == 'l') { // iter.remove(); // } // } // // Table resize may occur in ModIterator's destructor. using ModIterator = typename Impl::ModIterator;
ModIterator modIter() { return mImpl.modIter(); }
// These are similar to Iterator/ModIterator/iter(), but use different // terminology. using Range = typename Impl::Range; usingEnum = typename Impl::Enum;
Range all() const { return mImpl.all(); }
};
// HashSet is a fast hash-based set of values. // // Template parameter requirements: // - T: movable, destructible, assignable. // - HashPolicy: see the "Hash Policy" section below. // - AllocPolicy: see AllocPolicy.h // // Note: // - HashSet is not reentrant: T/HashPolicy/AllocPolicy members called by // HashSet must not call back into the same HashSet object. // template <class T, class HashPolicy = DefaultHasher<T>, class AllocPolicy = MallocAllocPolicy> class HashSet { // -- Implementation details -----------------------------------------------
// HashSet is not copyable or assignable.
HashSet(const HashSet& hs) = delete;
HashSet& operator=(const HashSet& hs) = delete;
struct SetHashPolicy : HashPolicy { using Base = HashPolicy; using KeyType = T;
// Swap the contents of this hash set with another. void swap(HashSet& aOther) { mImpl.swap(aOther.mImpl); }
// -- Status and sizing ----------------------------------------------------
// The set's current generation.
Generation generation() const { return mImpl.generation(); }
// Is the set empty? bool empty() const { return mImpl.empty(); }
// Number of elements in the set.
uint32_t count() const { return mImpl.count(); }
// Number of element slots in the set. Note: resize will happen well before // count() == capacity().
uint32_t capacity() const { return mImpl.capacity(); }
// The size of the set's entry storage, in bytes. If the elements contain // pointers to other heap blocks, you must iterate over the set and measure // them separately; hence the "shallow" prefix.
size_t shallowSizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const { return mImpl.shallowSizeOfExcludingThis(aMallocSizeOf);
}
size_t shallowSizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const { return aMallocSizeOf(this) +
mImpl.shallowSizeOfExcludingThis(aMallocSizeOf);
}
// Attempt to minimize the capacity(). If the table is empty, this will free // the empty storage and upon regrowth it will be given the minimum capacity. void compact() { mImpl.compact(); }
// Attempt to reserve enough space to fit at least |aLen| elements. This is // total capacity, including elements already present. Does nothing if the // map already has sufficient capacity.
[[nodiscard]] bool reserve(uint32_t aLen) { return mImpl.reserve(aLen); }
// Does the set contain an element matching |aLookup|? bool has(const Lookup& aLookup) const { return mImpl.lookup(aLookup).found();
}
// Return a Ptr indicating whether an element matching |aLookup| is present // in the set. E.g.: // // using HS = HashSet<int>; // HS h; // if (HS::Ptr p = h.lookup(3)) { // assert(*p == 3); // p acts like a pointer to int // } // using Ptr = typename Impl::Ptr;
MOZ_ALWAYS_INLINE Ptr lookup(const Lookup& aLookup) const { return mImpl.lookup(aLookup);
}
// Like lookup(), but does not assert if two threads call it at the same // time. Only use this method when none of the threads will modify the set.
MOZ_ALWAYS_INLINE Ptr readonlyThreadsafeLookup(const Lookup& aLookup) const { return mImpl.readonlyThreadsafeLookup(aLookup);
}
// Add |aU| if it is not present already. Returns false on OOM. template <typename U>
[[nodiscard]] bool put(U&& aU) {
AddPtr p = lookupForAdd(aU); return p ? true : add(p, std::forward<U>(aU));
}
// Like put(), but slightly faster. Must only be used when the given element // is not already present. (In debug builds, assertions check this.) template <typename U>
[[nodiscard]] bool putNew(U&& aU) { return mImpl.putNew(aU, std::forward<U>(aU));
}
// Like the other putNew(), but for when |Lookup| is different to |T|. template <typename U>
[[nodiscard]] bool putNew(const Lookup& aLookup, U&& aU) { return mImpl.putNew(aLookup, std::forward<U>(aU));
}
// Like putNew(), but should be only used when the table is known to be big // enough for the insertion, and hashing cannot fail. Typically this is used // to populate an empty set with known-unique elements after reserving space // with reserve(), e.g. // // using HS = HashMap<int>; // HS h; // if (!h.reserve(3)) { // MOZ_CRASH("OOM"); // } // h.putNewInfallible(1); // unique element // h.putNewInfallible(2); // unique element // h.putNewInfallible(3); // unique element // template <typename U> void putNewInfallible(const Lookup& aLookup, U&& aU) {
mImpl.putNewInfallible(aLookup, std::forward<U>(aU));
}
// Like |lookup(l)|, but on miss, |p = lookupForAdd(l)| allows efficient // insertion of T value |t| (where |HashPolicy::match(t,l) == true|) using // |add(p,t)|. After |add(p,t)|, |p| points to the new element. E.g.: // // using HS = HashSet<int>; // HS h; // HS::AddPtr p = h.lookupForAdd(3); // if (!p) { // if (!h.add(p, 3)) { // return false; // } // } // assert(*p == 3); // p acts like a pointer to int // // N.B. The caller must ensure that no mutating hash table operations occur // between a pair of lookupForAdd() and add() calls. To avoid looking up the // key a second time, the caller may use the more efficient relookupOrAdd() // method. This method reuses part of the hashing computation to more // efficiently insert the key if it has not been added. For example, a // mutation-handling version of the previous example: // // HS::AddPtr p = h.lookupForAdd(3); // if (!p) { // call_that_may_mutate_h(); // if (!h.relookupOrAdd(p, 3, 3)) { // return false; // } // } // assert(*p == 3); // // Note that relookupOrAdd(p,l,t) performs Lookup using |l| and adds the // entry |t|, where the caller ensures match(l,t). using AddPtr = typename Impl::AddPtr;
MOZ_ALWAYS_INLINE AddPtr lookupForAdd(const Lookup& aLookup) { return mImpl.lookupForAdd(aLookup);
}
// Lookup and remove the element matching |aLookup|, if present. void remove(const Lookup& aLookup) { if (Ptr p = lookup(aLookup)) {
remove(p);
}
}
// Remove a previously found element (assuming aPtr.found()). The set must // not have been mutated in the interim. void remove(Ptr aPtr) { mImpl.remove(aPtr); }
// Remove all keys/values without changing the capacity. void clear() { mImpl.clear(); }
// Like clear() followed by compact(). void clearAndCompact() { mImpl.clearAndCompact(); }
// Infallibly rekey one entry, if present. Requires that template parameters // T and HashPolicy::Lookup are the same type. void rekeyIfMoved(const Lookup& aOldValue, const T& aNewValue) { if (aOldValue != aNewValue) {
rekeyAs(aOldValue, aNewValue, aNewValue);
}
}
// Infallibly rekey one entry if present, and return whether that happened. bool rekeyAs(const Lookup& aOldLookup, const Lookup& aNewLookup, const T& aNewValue) { if (Ptr p = lookup(aOldLookup)) {
mImpl.rekeyAndMaybeRehash(p, aNewLookup, aNewValue); returntrue;
} returnfalse;
}
// Infallibly replace the current key at |aPtr| with an equivalent key. // Specifically, both HashPolicy::hash and HashPolicy::match must return // identical results for the new and old key when applied against all // possible matching values. void replaceKey(Ptr aPtr, const Lookup& aLookup, const T& aNewValue) {
MOZ_ASSERT(aPtr.found());
MOZ_ASSERT(*aPtr != aNewValue);
MOZ_ASSERT(HashPolicy::match(*aPtr, aLookup));
MOZ_ASSERT(HashPolicy::match(aNewValue, aLookup)); const_cast<T&>(*aPtr) = aNewValue;
MOZ_ASSERT(*lookup(aLookup) == aNewValue);
} void replaceKey(Ptr aPtr, const T& aNewValue) {
replaceKey(aPtr, aNewValue, aNewValue);
}
// |iter()| returns an Iterator: // // HashSet<int> h; // for (auto iter = h.iter(); !iter.done(); iter.next()) { // int i = iter.get(); // } // using Iterator = typename Impl::Iterator;
Iterator iter() const { return mImpl.iter(); }
// |modIter()| returns a ModIterator: // // HashSet<int> h; // for (auto iter = h.modIter(); !iter.done(); iter.next()) { // if (iter.get() == 42) { // iter.remove(); // } // } // // Table resize may occur in ModIterator's destructor. using ModIterator = typename Impl::ModIterator;
ModIterator modIter() { return mImpl.modIter(); }
// These are similar to Iterator/ModIterator/iter(), but use different // terminology. using Range = typename Impl::Range; usingEnum = typename Impl::Enum;
Range all() const { return mImpl.all(); }
};
// A hash policy |HP| for a hash table with key-type |Key| must provide: // // - a type |HP::Lookup| to use to lookup table entries; // // - a static member function |HP::hash| that hashes lookup values: // // static mozilla::HashNumber hash(const Lookup&); // // - a static member function |HP::match| that tests equality of key and // lookup values: // // static bool match(const Key& aKey, const Lookup& aLookup); // // |aKey| and |aLookup| can have different hash numbers, only when a // collision happens with |prepareHash| operation, which is less frequent. // Thus, |HP::match| shouldn't assume the hash equality in the comparison, // even if the hash numbers are almost always same between them. // // Normally, Lookup = Key. In general, though, different values and types of // values can be used to lookup and store. If a Lookup value |l| is not equal // to the added Key value |k|, the user must ensure that |HP::match(k,l)| is // true. E.g.: // // mozilla::HashSet<Key, HP>::AddPtr p = h.lookup(l); // if (!p) { // assert(HP::match(k, l)); // must hold // h.add(p, k); // }
// A pointer hashing policy that uses HashGeneric() to create good hashes for // pointers. Note that we don't shift out the lowest k bits because we don't // want to assume anything about the alignment of the pointers. template <typename Key> struct PointerHasher { using Lookup = Key;
// The default hash policy, which only works with integers. template <class Key, typename> struct DefaultHasher { using Lookup = Key;
static HashNumber hash(const Lookup& aLookup) { // Just convert the integer to a HashNumber and use that as is. (This // discards the high 32-bits of 64-bit integers!) ScrambleHashCode() is // subsequently called on the value to improve the distribution. return aLookup;
}
staticbool match(const Key& aKey, const Lookup& aLookup) { // Use builtin or overloaded operator==. return aKey == aLookup;
}
// A DefaultHasher specialization for pointers. template <class T> struct DefaultHasher<T*> : PointerHasher<T*> {};
// A DefaultHasher specialization for mozilla::UniquePtr. template <class T, class D> struct DefaultHasher<UniquePtr<T, D>> { using Key = UniquePtr<T, D>; using Lookup = Key; using PtrHasher = PointerHasher<T*>;
// A DefaultHasher specialization for doubles. template <> struct DefaultHasher<double> { using Key = double; using Lookup = Key;
static HashNumber hash(const Lookup& aLookup) { // Just xor the high bits with the low bits, and then treat the bits of the // result as a uint32_t.
static_assert(sizeof(HashNumber) == 4, "subsequent code assumes a four-byte hash");
uint64_t u = BitwiseCast<uint64_t>(aLookup); return HashNumber(u ^ (u >> 32));
}
// A DefaultHasher specialization for floats. template <> struct DefaultHasher<float> { using Key = float; using Lookup = Key;
static HashNumber hash(const Lookup& aLookup) { // Just use the value as if its bits form an integer. ScrambleHashCode() is // subsequently called on the value to improve the distribution.
static_assert(sizeof(HashNumber) == 4, "subsequent code assumes a four-byte hash"); return HashNumber(BitwiseCast<uint32_t>(aLookup));
}
// Most of the time generating a hash code is infallible, but sometimes it is // necessary to generate hash codes on demand in a way that can fail. Specialize // this class for your own hash policy to provide fallible hashing. // // This is used by MovableCellHasher to handle the fact that generating a unique // ID for cell pointer may fail due to OOM. // // The default implementations of these methods delegate to the usual HashPolicy // implementation and always succeed. template <typename HashPolicy> struct FallibleHashMethods { // Return true if a hashcode is already available for its argument, and // sets |aHashOut|. Once this succeeds for a specific argument it // must continue to do so. // // Return false if a hashcode is not already available. This implies that any // lookup must fail, as the hash code would have to have been successfully // created on insertion. template <typename Lookup> staticbool maybeGetHash(Lookup&& aLookup, HashNumber* aHashOut) {
*aHashOut = HashPolicy::hash(aLookup); returntrue;
}
// Fallible method to ensure a hashcode exists for its argument and create one // if not. Sets |aHashOut| to the hashcode and retuns true on success. Returns // false on error, e.g. out of memory. template <typename Lookup> staticbool ensureHash(Lookup&& aLookup, HashNumber* aHashOut) {
*aHashOut = HashPolicy::hash(aLookup); returntrue;
}
};
// Both HashMap and HashSet are implemented by a single HashTable that is even // more heavily parameterized than the other two. This leaves HashTable gnarly // and extremely coupled to HashMap and HashSet; thus code should not use // HashTable directly.
template <class Key, class Value> class HashMapEntry {
Key key_;
Value value_;
// Use this method with caution! If the key is changed such that its hash // value also changes, the map will be left in an invalid state.
Key& mutableKey() { return key_; }
template <class T, class HashPolicy, class AllocPolicy> class HashTable;
template <typename T> class EntrySlot;
template <typename T> class HashTableEntry { private: using NonConstT = std::remove_const_t<T>;
// Instead of having a hash table entry store that looks like this: // // +--------+--------+--------+--------+ // | entry0 | entry1 | .... | entryN | // +--------+--------+--------+--------+ // // where the entries contained their cached hash code, we're going to lay out // the entry store thusly: // // +-------+-------+-------+-------+--------+--------+--------+--------+ // | hash0 | hash1 | ... | hashN | entry0 | entry1 | .... | entryN | // +-------+-------+-------+-------+--------+--------+--------+--------+ // // with all the cached hashes prior to the actual entries themselves. // // We do this because implementing the first strategy requires us to make // HashTableEntry look roughly like: // // template <typename T> // class HashTableEntry { // HashNumber mKeyHash; // T mValue; // }; // // The problem with this setup is that, depending on the layout of `T`, there // may be platform ABI-mandated padding between `mKeyHash` and the first // member of `T`. This ABI-mandated padding is wasted space, and can be // surprisingly common, e.g. when `T` is a single pointer on 64-bit platforms. // In such cases, we're throwing away a quarter of our entry store on padding, // which is undesirable. // // The second layout above, namely: // // +-------+-------+-------+-------+--------+--------+--------+--------+ // | hash0 | hash1 | ... | hashN | entry0 | entry1 | .... | entryN | // +-------+-------+-------+-------+--------+--------+--------+--------+ // // means there is no wasted space between the hashes themselves, and no wasted // space between the entries themselves. However, we would also like there to // be no gap between the last hash and the first entry. The memory allocator // guarantees the alignment of the start of the hashes. The use of a // power-of-two capacity of at least 4 guarantees that the alignment of the // *end* of the hash array is no less than the alignment of the start. // Finally, the static_asserts here guarantee that the entries themselves // don't need to be any more aligned than the alignment of the entry store // itself. // // This assertion is safe for 32-bit builds because on both Windows and Linux // (including Android), the minimum alignment for allocations larger than 8 // bytes is 8 bytes, and the actual data for entries in our entry store is // guaranteed to have that alignment as well, thanks to the power-of-two // number of cached hash values stored prior to the entry data.
// The allocation policy must allocate a table with at least this much // alignment. static constexpr size_t kMinimumAlignment = 8;
static_assert(alignof(HashNumber) <= kMinimumAlignment, "[N*2 hashes, N*2 T values] allocation's alignment must be " "enough to align each hash");
static_assert(alignof(NonConstT) <= 2 * sizeof(HashNumber), "subsequent N*2 T values must not require more than an even " "number of HashNumbers provides");
// Some versions of GCC treat it as a -Wstrict-aliasing violation (ergo a // -Werror compile error) to reinterpret_cast<> |mValueData| to |T*|, even // through |void*|. Placing the latter cast in these separate functions // breaks the chain such that affected GCC versions no longer warn/error. void* rawValuePtr() { return mValueData; }
void swap(HashTableEntry* aOther, bool aIsLive) { // This allows types to use Argument-Dependent-Lookup, and thus use a custom // std::swap, which is needed by types like JS::Heap and such. using std::swap;
// A slot represents a cached hash value and its associated entry stored // in the hash table. These two things are not stored in contiguous memory. template <class T> class EntrySlot { using NonConstT = std::remove_const_t<T>;
template <class T, class HashPolicy, class AllocPolicy> class HashTable : private AllocPolicy { friendclass mozilla::ReentrancyGuard;
using NonConstT = std::remove_const_t<T>; using Key = typename HashPolicy::KeyType; using Lookup = typename HashPolicy::Lookup;
public: using Entry = HashTableEntry<T>; using Slot = EntrySlot<T>;
template <typename F> staticvoid forEachSlot(char* aTable, uint32_t aCapacity, F&& f) { auto hashes = reinterpret_cast<HashNumber*>(aTable); auto entries = reinterpret_cast<Entry*>(&hashes[aCapacity]);
Slot slot(entries, hashes); for (size_t i = 0; i < size_t(aCapacity); ++i) {
f(slot);
++slot;
}
}
// A nullable pointer to a hash table element. A Ptr |p| can be tested // either explicitly |if (p.found()) p->...| or using boolean conversion // |if (p) p->...|. Ptr objects must not be used after any mutating hash // table operations unless |generation()| is tested. class Ptr { friendclass HashTable;
// This constructor is used only by AddPtr() within lookupForAdd(). explicit Ptr(const HashTable& aTable)
: mSlot(nullptr, nullptr) #ifdef DEBUG
,
mTable(&aTable),
mGeneration(aTable.generation()) #endif
{
}
// This constructor is used when lookupForAdd() is performed on a table // lacking entry storage; it leaves mSlot null but initializes everything // else.
AddPtr(const HashTable& aTable, HashNumber aHashNumber)
: Ptr(aTable),
mKeyHash(aHashNumber) #ifdef DEBUG
,
mMutationCount(aTable.mMutationCount) #endif
{
MOZ_ASSERT(isLive());
}
// A hash table iterator that (mostly) doesn't allow table modifications. // As with Ptr/AddPtr, Iterator objects must not be used after any mutating // hash table operation unless the |generation()| is tested. class Iterator { void moveToNextLiveEntry() { while (++mCur < mEnd && !mCur.isLive()) { continue;
}
}
// A hash table iterator that permits modification, removal and rekeying. // Since rehashing when elements were removed during enumeration would be // bad, it is postponed until the ModIterator is destructed. Since the // ModIterator's destructor touches the hash table, the user must ensure // that the hash table is still alive when the destructor runs. class ModIterator : public Iterator { friendclass HashTable;
HashTable& mTable; bool mRekeyed; bool mRemoved;
// ModIterator is movable but not copyable.
ModIterator(const ModIterator&) = delete; voidoperator=(const ModIterator&) = delete;
// Removes the current element from the table, leaving |get()| // invalid until the next call to |next()|. void remove() {
mTable.remove(this->mCur);
mRemoved = true; #ifdef DEBUG
this->mValidEntry = false;
this->mMutationCount = mTable.mMutationCount; #endif
}
// Removes the current element and re-inserts it into the table with // a new key at the new Lookup position. |get()| is invalid after // this operation until the next call to |next()|. void rekey(const Lookup& l, const Key& k) {
MOZ_ASSERT(&k != &HashPolicy::getKey(this->mCur.get()));
Ptr p(this->mCur, mTable);
mTable.rekeyWithoutRehash(p, l, k);
mRekeyed = true; #ifdef DEBUG
this->mValidEntry = false;
this->mMutationCount = mTable.mMutationCount; #endif
}
void rekey(const Key& k) { rekey(k, k); }
// Potentially rehashes the table.
~ModIterator() { if (mRekeyed) {
mTable.mGen++;
mTable.infallibleRehashIfOverloaded();
}
if (mRemoved) {
mTable.compact();
}
}
};
// Range is similar to Iterator, but uses different terminology. class Range { friendclass HashTable;
// The default initial capacity is 32 (enough to hold 16 elements), but it // can be as low as 4. staticconst uint32_t sDefaultLen = 16; staticconst uint32_t sMinCapacity = 4; // See the comments in HashTableEntry about this value.
static_assert(sMinCapacity >= 4, "too-small sMinCapacity breaks assumptions"); staticconst uint32_t sMaxInit = 1u << (CAP_BITS - 1); staticconst uint32_t sMaxCapacity = 1u << CAP_BITS;
// Hash-table alpha is conceptually a fraction, but to avoid floating-point // math we implement it as a ratio of integers. staticconst uint8_t sAlphaDenominator = 4; staticconst uint8_t sMinAlphaNumerator = 1; // min alpha: 1/4 staticconst uint8_t sMaxAlphaNumerator = 3; // max alpha: 3/4
static uint32_t hashShift(uint32_t aLen) { // Reject all lengths whose initial computed capacity would exceed // sMaxCapacity. Round that maximum aLen down to the nearest power of two // for speedier code. if (MOZ_UNLIKELY(aLen > sMaxInit)) {
MOZ_CRASH("initial length is too large");
}
// Fake a struct that we're going to alloc. See the comments in // HashTableEntry about how the table is laid out, and why it's safe. struct FakeSlot { unsignedchar c[sizeof(HashNumber) + sizeof(typename Entry::NonConstT)];
};
Slot slotForIndex(HashNumber aIndex) const { auto hashes = reinterpret_cast<HashNumber*>(mTable); auto entries = reinterpret_cast<Entry*>(&hashes[capacity()]); return Slot(&entries[aIndex], &hashes[aIndex]);
}
// Warning: in order for readonlyThreadsafeLookup() to be safe this // function must not modify the table in any way when Reason==ForNonAdd. template <LookupReason Reason>
MOZ_ALWAYS_INLINE Slot lookup(const Lookup& aLookup,
HashNumber aKeyHash) const {
MOZ_ASSERT(isLiveHash(aKeyHash));
MOZ_ASSERT(!(aKeyHash & sCollisionBit));
MOZ_ASSERT(mTable);
// This is a copy of lookup() hardcoded to the assumptions: // 1. the lookup is for an add; // 2. the key, whose |keyHash| has been passed, is not in the table.
Slot findNonLiveSlot(HashNumber aKeyHash) {
MOZ_ASSERT(!(aKeyHash & sCollisionBit));
MOZ_ASSERT(mTable);
// We assume 'aKeyHash' has already been distributed.
// Look, but don't touch, until we succeed in getting new entry store. char* oldTable = mTable;
uint32_t oldCapacity = capacity();
uint32_t newLog2 = mozilla::CeilingLog2(newCapacity);
if (MOZ_UNLIKELY(newCapacity > sMaxCapacity)) { if (aReportFailure) {
this->reportAllocOverflow();
} return RehashFailed;
}
// Note: if capacity() is zero, this will always succeed, which is // what we want. bool overloaded = mEntryCount + mRemovedCount >=
capacity() * sMaxAlphaNumerator / sAlphaDenominator;
if (!overloaded) { return NotOverloaded;
}
// Succeed if a quarter or more of all entries are removed. Note that this // always succeeds if capacity() == 0 (i.e. entry storage has not been // allocated), which is what we want, because it means changeTableSize() // will allocate the requested capacity rather than doubling it. bool manyRemoved = mRemovedCount >= (capacity() >> 2);
uint32_t newCapacity = manyRemoved ? rawCapacity() : rawCapacity() * 2; return changeTableSize(newCapacity, aReportFailure);
}
if (underloaded) {
(void)changeTableSize(capacity() / 2, DontReportFailure);
}
}
// This is identical to changeTableSize(currentSize), but without requiring // a second table. We do this by recycling the collision bits to tell us if // the element is already inserted or still waiting to be inserted. Since // already-inserted elements win any conflicts, we get the same table as we // would have gotten through random insertion order. void rehashTableInPlace() {
mRemovedCount = 0;
mGen++;
forEachSlot(mTable, capacity(), [&](Slot& slot) { slot.unsetCollision(); }); for (uint32_t i = 0; i < capacity();) {
Slot src = slotForIndex(i);
if (!src.isLive() || src.hasCollision()) {
++i; continue;
}
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung ist noch experimentell.