/* -*- 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/. */ #ifndef SmallArrayLRUCache_h #define SmallArrayLRUCache_h #include "mozilla/Mutex.h" #include #include #include // Uncomment the next line to get shutdown stats about cache usage. // #define SMALLARRAYLRUCACHE_STATS #ifdef SMALLARRAYLRUCACHE_STATS # include #endif namespace mozilla { // Array-based LRU cache, thread-safe. // Optimized for cases where `FetchOrAdd` is used with the same key most // recently, and assuming the cost of running the value-builder function is much // more expensive than going through the whole list. // Note: No time limits on keeping items. // TODO: Move to more public place, if this could be used elsewhere; make sure // the cost/benefits are highlighted. template class SmallArrayLRUCache { public: static_assert(std::is_default_constructible_v); static_assert(std::is_trivially_constructible_v); static_assert(std::is_trivially_copyable_v); static_assert(std::is_default_constructible_v); static_assert(LRUCapacity >= 2); static_assert(LRUCapacity <= 1024, "This seems a bit big, is this the right cache for your use?"); // Reset all stored values to their default, and set cache size to zero. void Clear() { mozilla::OffTheBooksMutexAutoLock lock(mMutex); if (mSize == ShutdownSize) { return; } Clear(lock); } // Clear the cache, and then prevent further uses (other functions will do // nothing). void Shutdown() { mozilla::OffTheBooksMutexAutoLock lock(mMutex); if (mSize == ShutdownSize) { return; } Clear(lock); mSize = ShutdownSize; } // Add a key-value. template void Add(Key aKey, ToValue&& aValue) { mozilla::OffTheBooksMutexAutoLock lock(mMutex); if (mSize == ShutdownSize) { return; } // Quick add to the front, don't remove possible duplicate handles later in // the list, they will eventually drop off the end. KeyAndValue* const item0 = &mLRUArray[0]; mSize = std::min(mSize + 1, LRUCapacity); if (MOZ_LIKELY(mSize != 1)) { // List is not empty. // Make a hole at the start. std::move_backward(item0, item0 + mSize - 1, item0 + mSize); } item0->mKey = aKey; item0->mValue = std::forward(aValue); return; } // Look for the value associated with `aKey` in the cache. // If not found, run `aValueFunction()`, add it in the cache before returning. // After shutdown, just run `aValueFunction()`. template Value FetchOrAdd(Key aKey, ValueFunction&& aValueFunction) { Value value; mozilla::OffTheBooksMutexAutoLock lock(mMutex); if (mSize == ShutdownSize) { value = std::forward(aValueFunction)(); return value; } KeyAndValue* const item0 = &mLRUArray[0]; if (MOZ_UNLIKELY(mSize == 0)) { // List is empty. value = std::forward(aValueFunction)(); item0->mKey = aKey; item0->mValue = value; mSize = 1; return value; } if (MOZ_LIKELY(item0->mKey == aKey)) { // This is already at the beginning of the list, we're done. #ifdef SMALLARRAYLRUCACHE_STATS ++mCacheFoundAt[0]; #endif // SMALLARRAYLRUCACHE_STATS value = item0->mValue; return value; } for (KeyAndValue* item = item0 + 1; item != item0 + mSize; ++item) { if (item->mKey == aKey) { // Found handle in the middle. #ifdef SMALLARRAYLRUCACHE_STATS ++mCacheFoundAt[unsigned(item - item0)]; #endif // SMALLARRAYLRUCACHE_STATS value = item->mValue; // Move this item to the start of the list. std::rotate(item0, item, item + 1); return value; } } // Handle was not in the list. #ifdef SMALLARRAYLRUCACHE_STATS ++mCacheFoundAt[LRUCapacity]; #endif // SMALLARRAYLRUCACHE_STATS { // Don't lock while doing the potentially-expensive ValueFunction(). // This means that the list could change when we lock again, but // it's okay because we'll want to add the new entry at the beginning // anyway, whatever else is in the list then. // In the worst case, it could be the same handle as another `FetchOrAdd` // in parallel, it just means it will be duplicated, so it's a little bit // less efficient (using the extra space), but not wrong (the next // `FetchOrAdd` will find the first one). mozilla::OffTheBooksMutexAutoUnlock unlock(mMutex); value = std::forward(aValueFunction)(); } // Make a hole at the start, and put the value there. mSize = std::min(mSize + 1, LRUCapacity); std::move_backward(item0, item0 + mSize - 1, item0 + mSize); item0->mKey = aKey; item0->mValue = value; return value; } #ifdef SMALLARRAYLRUCACHE_STATS ~SmallArrayLRUCache() { if (mSize != 0 && mSize != ShutdownSize) { fprintf(stderr, "***** SmallArrayLRUCache stats: (position -> hit count)\n"); for (unsigned i = 0; i < mSize; ++i) { fprintf(stderr, "***** %3u -> %6u\n", i, mCacheFoundAt[i]); } fprintf(stderr, "***** not found -> %6u\n", mCacheFoundAt[LRUCapacity]); } } #endif // SMALLARRAYLRUCACHE_STATS private: void Clear(const mozilla::OffTheBooksMutexAutoLock&) MOZ_REQUIRES(mMutex) { for (KeyAndValue* item = &mLRUArray[0]; item != &mLRUArray[mSize]; ++item) { item->mValue = Value{}; } mSize = 0; } struct KeyAndValue { Key mKey; Value mValue; KeyAndValue() = default; KeyAndValue(KeyAndValue&&) = default; KeyAndValue& operator=(KeyAndValue&&) = default; }; // Special size value that indicates the cache is in shutdown mode. constexpr static unsigned ShutdownSize = unsigned(-1); mozilla::OffTheBooksMutex mMutex{"LRU cache"}; unsigned mSize MOZ_GUARDED_BY(mMutex) = 0; KeyAndValue mLRUArray[LRUCapacity] MOZ_GUARDED_BY(mMutex); #ifdef SMALLARRAYLRUCACHE_STATS // Hit count for each position in the case. +1 for counting not-found cases. unsigned mCacheFoundAt[LRUCapacity + 1] MOZ_GUARDED_BY(mMutex) = {0u}; #endif // SMALLARRAYLRUCACHE_STATS }; } // namespace mozilla #endif // SmallArrayLRUCache_h