diff --git a/dom/canvas/CanvasRenderingContext2D.h b/dom/canvas/CanvasRenderingContext2D.h index ea7a2fd4559f..1fd634b51d7a 100644 --- a/dom/canvas/CanvasRenderingContext2D.h +++ b/dom/canvas/CanvasRenderingContext2D.h @@ -1208,6 +1208,8 @@ class CanvasRenderingContext2D : public nsICanvasRenderingContextInternal, class FontStyleCache : public MruCache { public: + // A cached failure has a null mStyle, but every live entry has a lang. + static bool IsEmpty(const FontStyleData& aVal) { return !aVal.mKey.mLang; } static HashNumber Hash(const FontStyleCacheKey& aKey) { HashNumber hash = HashString(aKey.mFont); hash = AddToHash(hash, aKey.mLang->hash()); @@ -1250,6 +1252,9 @@ class CanvasRenderingContext2D : public nsICanvasRenderingContextInternal, class FontGroupCache : public MruCache { public: + static bool IsEmpty(const FontGroupCacheData& aVal) { + return !aVal.mFontGroup; + } static HashNumber Hash(const FontGroupCacheKey& aKey) { HashNumber hash = HashString(aKey.mSpecifiedFont); hash = AddToHash(hash, aKey.mGeneration); @@ -1277,6 +1282,9 @@ class CanvasRenderingContext2D : public nsICanvasRenderingContextInternal, class ColorStyleCache : public MruCache { public: + static bool IsEmpty(const ColorStyleCacheEntry& aVal) { + return aVal.mKey.IsEmpty(); + } static HashNumber Hash(const nsACString& aKey) { return HashString(aKey); } static bool Match(const nsACString& aKey, const ColorStyleCacheEntry& aVal) { diff --git a/gfx/thebes/FontPaletteCache.h b/gfx/thebes/FontPaletteCache.h index bcc476b8e6f8..816119227ea9 100644 --- a/gfx/thebes/FontPaletteCache.h +++ b/gfx/thebes/FontPaletteCache.h @@ -56,6 +56,7 @@ class PaletteCache already_AddRefed GetPaletteFor(gfxFontEntry* aFontEntry, nsAtom* aPaletteName); + static bool IsEmpty(const CacheData& aVal) { return !aVal.mKey.first; } static mozilla::HashNumber Hash(const CacheKey& aKey) { return mozilla::HashGeneric(aKey.first.get(), aKey.second.get()); } diff --git a/gfx/thebes/gfxHarfBuzzShaper.h b/gfx/thebes/gfxHarfBuzzShaper.h index dbe3f2334b00..e6d43b6a3998 100644 --- a/gfx/thebes/gfxHarfBuzzShaper.h +++ b/gfx/thebes/gfxHarfBuzzShaper.h @@ -187,6 +187,9 @@ class gfxHarfBuzzShaper : public gfxFontShaper { struct CmapCache : public mozilla::MruCache { + static bool IsEmpty(const CmapCacheData& aData) { + return !aData.mCodepoint && !aData.mGlyphId; + } static mozilla::HashNumber Hash(const uint32_t& aKey) { return aKey; } static bool Match(const uint32_t& aKey, const CmapCacheData& aData) { return aKey == aData.mCodepoint; @@ -202,6 +205,9 @@ class gfxHarfBuzzShaper : public gfxFontShaper { struct WidthCache : public mozilla::MruCache { + static bool IsEmpty(const WidthCacheData& aData) { + return !aData.mGlyphId && !aData.mAdvance; + } static mozilla::HashNumber Hash(const hb_codepoint_t& aKey) { return aKey; } static bool Match(const uint32_t& aKey, const WidthCacheData& aData) { return aKey == aData.mGlyphId; diff --git a/gfx/thebes/gfxTextRun.cpp b/gfx/thebes/gfxTextRun.cpp index 5b68d6602b4d..f2c42a498e7f 100644 --- a/gfx/thebes/gfxTextRun.cpp +++ b/gfx/thebes/gfxTextRun.cpp @@ -2585,6 +2585,9 @@ static Script ResolveScriptForLang(const nsAtom* aLanguage, Script aDefault) { : public MruCache, LangScriptCache> { public: + static bool IsEmpty(const std::pair& aValue) { + return !aValue.first; + } static HashNumber Hash(const nsAtom* const& aKey) { return aKey->hash(); } static bool Match(const nsAtom* const& aKey, const std::pair& aValue) { diff --git a/intl/lwbrk/LineBreakCache.h b/intl/lwbrk/LineBreakCache.h index d12d702ac97c..a9c4bbc839d4 100644 --- a/intl/lwbrk/LineBreakCache.h +++ b/intl/lwbrk/LineBreakCache.h @@ -15,8 +15,7 @@ #include "mozilla/StaticPtr.h" #include "mozilla/intl/Segmenter.h" -namespace mozilla { -namespace intl { +namespace mozilla::intl { namespace detail { struct LBCacheKey { @@ -40,8 +39,6 @@ struct LBCacheEntry { // Most-recently-used cache for line-break results, because finding line- // breaks may be slow for complex writing systems (e.g. Thai, Khmer). -// The MruCache size should be a prime number that is slightly less than a -// power of two. class LineBreakCache : public MruCache { public: @@ -66,6 +63,10 @@ class LineBreakCache : public MruCache sBreakCache; }; -} // namespace intl -} // namespace mozilla +} // namespace mozilla::intl #endif /* mozilla_intl_LineBreakCache_h_ */ diff --git a/layout/painting/nsImageRenderer.cpp b/layout/painting/nsImageRenderer.cpp index 1fb3b57c2f47..7ddc4f7e2592 100644 --- a/layout/painting/nsImageRenderer.cpp +++ b/layout/painting/nsImageRenderer.cpp @@ -77,6 +77,7 @@ struct SymbolicImageEntry { struct SymbolicImageCache final : public mozilla::MruCache { + static bool IsEmpty(const ValueType& aVal) { return !std::get<0>(aVal.mKey); } static HashNumber Hash(const KeyType& aKey) { return AddToHash(std::get<0>(aKey)->hash(), HashGeneric(std::get<1>(aKey), std::get<2>(aKey))); diff --git a/mfbt/MruCache.h b/mfbt/MruCache.h index e51a5d57aad3..912bc41a1d8b 100644 --- a/mfbt/MruCache.h +++ b/mfbt/MruCache.h @@ -16,30 +16,17 @@ namespace mozilla { -namespace detail { - -// Helper struct for checking if a value is empty. -// -// `IsNotEmpty` will return true if `Value` is not a pointer type or if the -// pointer value is not null. -template -constexpr bool IsNotEmpty(const Value& aVal) { - if constexpr (!std::is_pointer_v) { - return true; - } else { - return aVal != nullptr; - } -} - -} // namespace detail - // Provides a most recently used cache that can be used as a layer on top of -// a larger container where lookups can be expensive. The size must be a power -// of two; entries are indexed by the high bits of the scrambled hash (Fibonacci -// hashing), so callers don't need to provide a well-distributed hash. +// a larger container where lookups can be expensive. // -// Users are expected to provide a `Cache` class that defines two required +// `Size` is the total number of entries and must be a power of two. The cache +// is set-associative: entries are grouped into sets, and the set is indexed by +// the high bits of the scrambled hash (Fibonacci hashing), so callers don't +// need to provide a well-distributed hash. +// +// Users are expected to provide a `Cache` class that defines the following // methods: +// // - A method for providing the hash of a key: // // static HashNumber Hash(const KeyType& aKey) @@ -49,6 +36,15 @@ constexpr bool IsNotEmpty(const Value& aVal) { // // static bool Match(const KeyType& aKey, const ValueType& aVal) // +// - A method telling whether a value is an unused entry. +// This is only required if `ValueType` is not a pointer type, for which +// null is used. `ValueType{}` must be empty, since that is what `Remove()` +// and `Clear()` leave behind. Reporting a live entry as empty is not +// incorrect, just wasteful: it will be treated as a free slot and thus +// never found again. +// +// static bool IsEmpty(const ValueType& aVal) +// // For example: // class MruExample : public MruCache // { @@ -61,12 +57,18 @@ constexpr bool IsNotEmpty(const Value& aVal) { // return aVal->mPtr == aKey; // } // }; -template +template class MruCache { - static_assert(Size >= 2 && (Size & (Size - 1)) == 0, - "Size must be a power of two"); - public: + // Associativity: how many entries a single key hash may occupy. + static constexpr size_t kWays = Ways; + + static_assert((Size & (Size - 1)) == 0, "Size must be a power of two"); + static_assert(Size > kWays, + "Size must be larger than the associativity, so that there's " + "more than one set"); + using KeyType = Key; using ValueType = Value; @@ -74,11 +76,21 @@ class MruCache { MruCache(const MruCache&) = delete; MruCache(const MruCache&&) = delete; - // Inserts the given value into the cache. Potentially overwrites an - // existing entry. + // Default implementation of the emptiness check described above, which + // `Cache` is expected to shadow for non-pointer value types. + static bool IsEmpty(const ValueType& aVal) { + static_assert(std::is_pointer_v, + "Non-pointer value types must provide " + "`static bool IsEmpty(const ValueType&)`"); + return !aVal; + } + + // Inserts the given value into the cache. Overwrites the existing entry for + // `aKey` if there is one, and otherwise potentially evicts an unrelated + // entry. template void Put(const KeyType& aKey, U&& aVal) { - *RawEntry(aKey) = std::forward(aVal); + Lookup(aKey).Set(std::forward(aVal)); } // Removes the given entry if it is in the cache. @@ -134,26 +146,39 @@ class MruCache { // Retrieves an entry from the cache. Can be used to test if an entry is // present, update the entry to a new value, or remove the entry if one was - // matched. - Entry Lookup(const KeyType& aKey) { - auto entry = RawEntry(aKey); - bool match = detail::IsNotEmpty(*entry) && Cache::Match(aKey, *entry); - return Entry(entry, match); + // matched. Does not modify the cache. + MOZ_ALWAYS_INLINE Entry Lookup(const KeyType& aKey) { + const HashNumber hash = ScrambleHashCode(Cache::Hash(aKey)); + const size_t base = SetIndex(hash) * kWays; + + size_t freeEntry = kWays; + for (size_t way = 0; way < kWays; ++way) { + ValueType& val = mCache[base + way]; + if (Cache::IsEmpty(val)) { + freeEntry = way; + continue; + } + if (Cache::Match(aKey, val)) { + return Entry(&val, true); + } + } + if (freeEntry == kWays) { + // The set is full, so evict an entry chosen by the low bits of the hash. + // TODO(emilio): Maybe better eviction policy? + freeEntry = hash & (kWays - 1); + } + return Entry(&mCache[base + freeEntry], false); } private: - static constexpr uint32_t kShift = kHashNumberBits - CeilingLog2(Size); - - MOZ_ALWAYS_INLINE ValueType* RawEntry(const KeyType& aKey) { - // Index using the high bits of the scrambled hash (Fibonacci hashing). This - // stays well-distributed even for the low-entropy hashes some callers - // provide, and avoids a modulo on this hot path. - return &mCache[ScrambleHashCode(Cache::Hash(aKey)) >> kShift]; - } + static constexpr size_t kSets = Size / kWays; + static constexpr uint32_t kSetBits = CeilingLog2(kSets); + static constexpr uint32_t kShift = kHashNumberBits - kSetBits; + static constexpr size_t SetIndex(HashNumber aHash) { return aHash >> kShift; } ValueType mCache[Size] = {}; }; } // namespace mozilla -#endif // mozilla_mrucache_h +#endif // mozilla_MruCache_h diff --git a/netwerk/dns/nsEffectiveTLDService.h b/netwerk/dns/nsEffectiveTLDService.h index 9076d906c1bd..d154000796a4 100644 --- a/netwerk/dns/nsEffectiveTLDService.h +++ b/netwerk/dns/nsEffectiveTLDService.h @@ -61,6 +61,9 @@ class nsEffectiveTLDService final : public nsIEffectiveTLDService { // mitigation getting about a 99% hit rate with four tabs open. struct TldCache : public mozilla::MruCache { + static bool IsEmpty(const TLDCacheEntry& aVal) { + return aVal.mHost.IsEmpty(); + } static mozilla::HashNumber Hash(const nsACString& aKey) { return mozilla::HashString(aKey); } diff --git a/xpcom/tests/gtest/TestMruCache.cpp b/xpcom/tests/gtest/TestMruCache.cpp index 992e6f327187..0632ad85dcee 100644 --- a/xpcom/tests/gtest/TestMruCache.cpp +++ b/xpcom/tests/gtest/TestMruCache.cpp @@ -12,6 +12,7 @@ using namespace mozilla; // A few MruCache implementations to use during testing. struct IntMap : public MruCache { static HashNumber Hash(const KeyType& aKey) { return aKey - 1; } + static bool IsEmpty(const ValueType& aVal) { return !aVal; } static bool Match(const KeyType& aKey, const ValueType& aVal) { return aKey == aVal; } @@ -34,20 +35,36 @@ struct StringStructMap static HashNumber Hash(const KeyType& aKey) { return *aKey.BeginReading() - 1; } + static bool IsEmpty(const ValueType& aVal) { return aVal.mKey.IsEmpty(); } static bool Match(const KeyType& aKey, const ValueType& aVal) { return aKey == aVal.mKey; } }; -// All keys map to the same slot, so any two distinct keys collide. Used to -// exercise eviction/overwrite behaviour independently of hash distribution. -struct CollideMap : public MruCache { +// Every key hashes into the same set, so all keys collide. Used to exercise +// coexistence and eviction independently of hash distribution. +struct CollideMap : public MruCache { static HashNumber Hash(const KeyType&) { return 0; } + static bool IsEmpty(const ValueType& aVal) { return !aVal; } static bool Match(const KeyType& aKey, const ValueType& aVal) { return aKey == aVal; } }; +// The number of keys CollideMap's single set holds at once. +static constexpr size_t kCollideWays = CollideMap::kWays; + +// Counts how many of the keys [1, kCollideWays] are still cached. +static size_t CountLive(CollideMap& aMru) { + size_t live = 0; + for (size_t i = 1; i <= kCollideWays; i++) { + if (aMru.Lookup(i)) { + live++; + } + } + return live; +} + // Helper for emulating convertable holders such as RefPtr. template struct Convertable { @@ -136,17 +153,20 @@ TEST(MruCache, TestPutConvertable) TEST(MruCache, TestOverwriting) { - // Distinct keys that map to the same slot evict each other. + // Distinct keys that map to the same set only evict each other once the set + // is full. CollideMap mru; + for (size_t i = 1; i <= kCollideWays; i++) { + mru.Put(i, i); + } + EXPECT_EQ(CountLive(mru), kCollideWays); - mru.Put(1, 1); - mru.Put(2, 2); // Evicts 1. + mru.Put(kCollideWays + 1, kCollideWays + 1); // Evicts one of the above. - EXPECT_FALSE(mru.Lookup(1)); - - auto p = mru.Lookup(2); + auto p = mru.Lookup(kCollideWays + 1); EXPECT_TRUE(p); - EXPECT_EQ(p.Data(), 2); + EXPECT_EQ(p.Data(), int(kCollideWays + 1)); + EXPECT_EQ(CountLive(mru), kCollideWays - 1); } TEST(MruCache, TestRemove) @@ -266,29 +286,28 @@ TEST(MruCache, TestLookupMissingAndSet) TEST(MruCache, TestLookupAndOverwrite) { - // Two distinct keys that map to the same slot. + // Fill up a set with keys that all collide. CollideMap mru; + for (size_t i = 1; i <= kCollideWays; i++) { + mru.Put(i, i); + } - // Set 1. - mru.Put(1, 1); - - // Lookup a different key that maps to 1's entry. - auto p = mru.Lookup(2); + // Lookup a key that maps to the same, now full, set. + const int key = kCollideWays + 1; + auto p = mru.Lookup(key); EXPECT_FALSE(p); // not a match - // Now overwrite the entry. - p.Set(2); + // Now overwrite the entry it picked. + p.Set(key); EXPECT_TRUE(p); - EXPECT_EQ(p.Data(), 2); + EXPECT_EQ(p.Data(), key); - // 1 should be gone now. - p = mru.Lookup(1); - EXPECT_FALSE(p); + // One of the previous keys should be gone now. + EXPECT_EQ(CountLive(mru), kCollideWays - 1); - // 2 should be found. - p = mru.Lookup(2); + p = mru.Lookup(key); EXPECT_TRUE(p); - EXPECT_EQ(p.Data(), 2); + EXPECT_EQ(p.Data(), key); } TEST(MruCache, TestLookupAndRemove) @@ -344,3 +363,28 @@ TEST(MruCache, TestLookupAndSetWithMove) EXPECT_TRUE(p.Data().mKey == key); EXPECT_TRUE(p.Data().mOther == "foo"_ns); } + +TEST(MruCache, TestAssociativity) +{ + CollideMap mru; + + for (size_t i = 1; i <= kCollideWays; i++) { + mru.Put(i, i); + } + + for (size_t i = 1; i <= kCollideWays; i++) { + auto p = mru.Lookup(i); + EXPECT_TRUE(p); + EXPECT_EQ(p.Data(), int(i)); + } +} + +TEST(MruCache, TestPutReusesMatchingEntry) +{ + CollideMap mru; + // Putting the same key should not create multiple entries. + mru.Put(1, 1); + mru.Put(1, 1); + mru.Remove(1); + EXPECT_FALSE(mru.Lookup(1)); +}