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:
committed by
ealvarez@mozilla.com
parent
57c6cf799a
commit
e2ccd3e02a
@@ -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) {
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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_ */
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user