Bug 2062332 - Make MruCache 2-way associative. r=layout-reviewers,jfkthame

So that we can at least hold two elements at a time with the same high
hash bits. Otherwise if you get unlucky you get trashing in reasonable
test-cases like the thai-reflow.html perftest.

Differential Revision: https://phabricator.services.mozilla.com/D320099
This commit is contained in:
Emilio Cobos Álvarez
2026-08-21 09:38:47 +00:00
committed by ealvarez@mozilla.com
parent 57c6cf799a
commit e2ccd3e02a
9 changed files with 164 additions and 73 deletions
+8
View File
@@ -1208,6 +1208,8 @@ class CanvasRenderingContext2D : public nsICanvasRenderingContextInternal,
class FontStyleCache
: public MruCache<FontStyleCacheKey, FontStyleData, FontStyleCache> {
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<FontGroupCacheKey, FontGroupCacheData, FontGroupCache> {
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<nsACString, ColorStyleCacheEntry, ColorStyleCache> {
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) {
+1
View File
@@ -56,6 +56,7 @@ class PaletteCache
already_AddRefed<FontPalette> 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());
}
+6
View File
@@ -187,6 +187,9 @@ class gfxHarfBuzzShaper : public gfxFontShaper {
struct CmapCache
: public mozilla::MruCache<uint32_t, CmapCacheData, CmapCache, 256> {
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<uint32_t, WidthCacheData, WidthCache, 256> {
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;
+3
View File
@@ -2585,6 +2585,9 @@ static Script ResolveScriptForLang(const nsAtom* aLanguage, Script aDefault) {
: public MruCache<const nsAtom*, std::pair<const nsAtom*, Script>,
LangScriptCache> {
public:
static bool IsEmpty(const std::pair<const nsAtom*, Script>& aValue) {
return !aValue.first;
}
static HashNumber Hash(const nsAtom* const& aKey) { return aKey->hash(); }
static bool Match(const nsAtom* const& aKey,
const std::pair<const nsAtom*, Script>& aValue) {
+6 -6
View File
@@ -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<detail::LBCacheKey, detail::LBCacheEntry,
LineBreakCache, 4096> {
public:
@@ -66,6 +63,10 @@ class LineBreakCache : public MruCache<detail::LBCacheKey, detail::LBCacheEntry,
return h;
}
static bool IsEmpty(const EntryType& aEntry) {
return aEntry.mText.IsEmpty();
}
static bool Match(const KeyType& aKey, const EntryType& aEntry) {
return nsDependentSubstring(aKey.mText, aKey.mLength)
.Equals(aEntry.mText) &&
@@ -93,7 +94,6 @@ class LineBreakCache : public MruCache<detail::LBCacheKey, detail::LBCacheEntry,
static StaticAutoPtr<LineBreakCache> sBreakCache;
};
} // namespace intl
} // namespace mozilla
} // namespace mozilla::intl
#endif /* mozilla_intl_LineBreakCache_h_ */
+1
View File
@@ -77,6 +77,7 @@ struct SymbolicImageEntry {
struct SymbolicImageCache final
: public mozilla::MruCache<SymbolicImageKey, SymbolicImageEntry,
SymbolicImageCache, 8> {
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)));
+67 -42
View File
@@ -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 <typename Value>
constexpr bool IsNotEmpty(const Value& aVal) {
if constexpr (!std::is_pointer_v<Value>) {
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<void*, PtrInfo*, MruExample>
// {
@@ -61,12 +57,18 @@ constexpr bool IsNotEmpty(const Value& aVal) {
// return aVal->mPtr == aKey;
// }
// };
template <class Key, class Value, class Cache, size_t Size = 32>
template <class Key, class Value, class Cache, size_t Size = 32,
size_t Ways = 2>
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<ValueType>,
"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 <typename U>
void Put(const KeyType& aKey, U&& aVal) {
*RawEntry(aKey) = std::forward<U>(aVal);
Lookup(aKey).Set(std::forward<U>(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
+3
View File
@@ -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<nsACString, TLDCacheEntry, TldCache> {
static bool IsEmpty(const TLDCacheEntry& aVal) {
return aVal.mHost.IsEmpty();
}
static mozilla::HashNumber Hash(const nsACString& aKey) {
return mozilla::HashString(aKey);
}
+69 -25
View File
@@ -12,6 +12,7 @@ using namespace mozilla;
// A few MruCache implementations to use during testing.
struct IntMap : public MruCache<int, int, IntMap> {
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<int, int, CollideMap> {
// 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<int, int, CollideMap, 8> {
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 <typename T>
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));
}