Cleaned up and simplified LRU cache implementation (#5579)

Summary:
The 'refs' field in LRUHandle now counts only external references, since anyway we already have the IN_CACHE flag. This simplifies reference accounting logic a bit. Also cleaned up few asserts code as well as the comments - to be more readable.
Pull Request resolved: https://github.com/facebook/rocksdb/pull/5579

Differential Revision: D16286747

Pulled By: elipoz

fbshipit-source-id: 7186d88f80f512ce584d0a303437494b5cbefd7f
This commit is contained in:
Eli Pozniansky 2019-07-16 19:13:35 -07:00 committed by Facebook Github Bot
parent 0f4d90e6e4
commit 74fb7f0ba5
4 changed files with 106 additions and 113 deletions

1
.gitignore vendored
View File

@ -32,6 +32,7 @@ ldb
manifest_dump manifest_dump
sst_dump sst_dump
blob_dump blob_dump
block_cache_trace_analyzer
column_aware_encoding_exp column_aware_encoding_exp
util/build_version.cc util/build_version.cc
build_tools/VALGRIND_LOGS/ build_tools/VALGRIND_LOGS/

4
cache/cache_test.cc vendored
View File

@ -562,6 +562,7 @@ TEST_P(CacheTest, SetStrictCapacityLimit) {
ASSERT_OK(s); ASSERT_OK(s);
ASSERT_NE(nullptr, handles[i]); ASSERT_NE(nullptr, handles[i]);
} }
ASSERT_EQ(10, cache->GetUsage());
// test2: set the flag to true. Insert and check if it fails. // test2: set the flag to true. Insert and check if it fails.
std::string extra_key = "extra"; std::string extra_key = "extra";
@ -571,6 +572,7 @@ TEST_P(CacheTest, SetStrictCapacityLimit) {
s = cache->Insert(extra_key, extra_value, 1, &deleter, &handle); s = cache->Insert(extra_key, extra_value, 1, &deleter, &handle);
ASSERT_TRUE(s.IsIncomplete()); ASSERT_TRUE(s.IsIncomplete());
ASSERT_EQ(nullptr, handle); ASSERT_EQ(nullptr, handle);
ASSERT_EQ(10, cache->GetUsage());
for (size_t i = 0; i < 10; i++) { for (size_t i = 0; i < 10; i++) {
cache->Release(handles[i]); cache->Release(handles[i]);
@ -591,7 +593,7 @@ TEST_P(CacheTest, SetStrictCapacityLimit) {
s = cache2->Insert(extra_key, extra_value, 1, &deleter); s = cache2->Insert(extra_key, extra_value, 1, &deleter);
// AS if the key have been inserted into cache but get evicted immediately. // AS if the key have been inserted into cache but get evicted immediately.
ASSERT_OK(s); ASSERT_OK(s);
ASSERT_EQ(5, cache->GetUsage()); ASSERT_EQ(5, cache2->GetUsage());
ASSERT_EQ(nullptr, cache2->Lookup(extra_key)); ASSERT_EQ(nullptr, cache2->Lookup(extra_key));
for (size_t i = 0; i < 5; i++) { for (size_t i = 0; i < 5; i++) {

134
cache/lru_cache.cc vendored
View File

@ -24,7 +24,7 @@ LRUHandleTable::LRUHandleTable() : list_(nullptr), length_(0), elems_(0) {
LRUHandleTable::~LRUHandleTable() { LRUHandleTable::~LRUHandleTable() {
ApplyToAllCacheEntries([](LRUHandle* h) { ApplyToAllCacheEntries([](LRUHandle* h) {
if (h->refs == 1) { if (!h->HasRefs()) {
h->Free(); h->Free();
} }
}); });
@ -113,29 +113,17 @@ LRUCacheShard::LRUCacheShard(size_t capacity, bool strict_capacity_limit,
SetCapacity(capacity); SetCapacity(capacity);
} }
LRUCacheShard::~LRUCacheShard() {}
bool LRUCacheShard::Unref(LRUHandle* e) {
assert(e->refs > 0);
e->refs--;
return e->refs == 0;
}
// Call deleter and free
void LRUCacheShard::EraseUnRefEntries() { void LRUCacheShard::EraseUnRefEntries() {
autovector<LRUHandle*> last_reference_list; autovector<LRUHandle*> last_reference_list;
{ {
MutexLock l(&mutex_); MutexLock l(&mutex_);
while (lru_.next != &lru_) { while (lru_.next != &lru_) {
LRUHandle* old = lru_.next; LRUHandle* old = lru_.next;
assert(old->InCache()); // LRU list contains only elements which can be evicted
assert(old->refs == assert(old->InCache() && !old->HasRefs());
1); // LRU list contains elements which may be evicted
LRU_Remove(old); LRU_Remove(old);
table_.Remove(old->key(), old->hash); table_.Remove(old->key(), old->hash);
old->SetInCache(false); old->SetInCache(false);
Unref(old);
usage_ -= old->charge; usage_ -= old->charge;
last_reference_list.push_back(old); last_reference_list.push_back(old);
} }
@ -148,22 +136,27 @@ void LRUCacheShard::EraseUnRefEntries() {
void LRUCacheShard::ApplyToAllCacheEntries(void (*callback)(void*, size_t), void LRUCacheShard::ApplyToAllCacheEntries(void (*callback)(void*, size_t),
bool thread_safe) { bool thread_safe) {
if (thread_safe) { const auto applyCallback = [&]() {
mutex_.Lock();
}
table_.ApplyToAllCacheEntries( table_.ApplyToAllCacheEntries(
[callback](LRUHandle* h) { callback(h->value, h->charge); }); [callback](LRUHandle* h) { callback(h->value, h->charge); });
};
if (thread_safe) { if (thread_safe) {
mutex_.Unlock(); MutexLock l(&mutex_);
applyCallback();
} else {
applyCallback();
} }
} }
void LRUCacheShard::TEST_GetLRUList(LRUHandle** lru, LRUHandle** lru_low_pri) { void LRUCacheShard::TEST_GetLRUList(LRUHandle** lru, LRUHandle** lru_low_pri) {
MutexLock l(&mutex_);
*lru = &lru_; *lru = &lru_;
*lru_low_pri = lru_low_pri_; *lru_low_pri = lru_low_pri_;
} }
size_t LRUCacheShard::TEST_GetLRUSize() { size_t LRUCacheShard::TEST_GetLRUSize() {
MutexLock l(&mutex_);
LRUHandle* lru_handle = lru_.next; LRUHandle* lru_handle = lru_.next;
size_t lru_size = 0; size_t lru_size = 0;
while (lru_handle != &lru_) { while (lru_handle != &lru_) {
@ -231,14 +224,13 @@ void LRUCacheShard::MaintainPoolSize() {
void LRUCacheShard::EvictFromLRU(size_t charge, void LRUCacheShard::EvictFromLRU(size_t charge,
autovector<LRUHandle*>* deleted) { autovector<LRUHandle*>* deleted) {
while (usage_ + charge > capacity_ && lru_.next != &lru_) { while ((usage_ + charge) > capacity_ && lru_.next != &lru_) {
LRUHandle* old = lru_.next; LRUHandle* old = lru_.next;
assert(old->InCache()); // LRU list contains only elements which can be evicted
assert(old->refs == 1); // LRU list contains elements which may be evicted assert(old->InCache() && !old->HasRefs());
LRU_Remove(old); LRU_Remove(old);
table_.Remove(old->key(), old->hash); table_.Remove(old->key(), old->hash);
old->SetInCache(false); old->SetInCache(false);
Unref(old);
usage_ -= old->charge; usage_ -= old->charge;
deleted->push_back(old); deleted->push_back(old);
} }
@ -252,8 +244,8 @@ void LRUCacheShard::SetCapacity(size_t capacity) {
high_pri_pool_capacity_ = capacity_ * high_pri_pool_ratio_; high_pri_pool_capacity_ = capacity_ * high_pri_pool_ratio_;
EvictFromLRU(0, &last_reference_list); EvictFromLRU(0, &last_reference_list);
} }
// we free the entries here outside of mutex for
// performance reasons // Free the entries outside of mutex for performance reasons
for (auto entry : last_reference_list) { for (auto entry : last_reference_list) {
entry->Free(); entry->Free();
} }
@ -269,22 +261,22 @@ Cache::Handle* LRUCacheShard::Lookup(const Slice& key, uint32_t hash) {
LRUHandle* e = table_.Lookup(key, hash); LRUHandle* e = table_.Lookup(key, hash);
if (e != nullptr) { if (e != nullptr) {
assert(e->InCache()); assert(e->InCache());
if (e->refs == 1) { if (!e->HasRefs()) {
// The entry is in LRU since it's in hash and has no external references
LRU_Remove(e); LRU_Remove(e);
} }
e->refs++; e->Ref();
e->SetHit(); e->SetHit();
} }
return reinterpret_cast<Cache::Handle*>(e); return reinterpret_cast<Cache::Handle*>(e);
} }
bool LRUCacheShard::Ref(Cache::Handle* h) { bool LRUCacheShard::Ref(Cache::Handle* h) {
LRUHandle* handle = reinterpret_cast<LRUHandle*>(h); LRUHandle* e = reinterpret_cast<LRUHandle*>(h);
MutexLock l(&mutex_); MutexLock l(&mutex_);
if (handle->InCache() && handle->refs == 1) { // To create another reference - entry must be already externally referenced
LRU_Remove(handle); assert(e->HasRefs());
} e->Ref();
handle->refs++;
return true; return true;
} }
@ -303,30 +295,27 @@ bool LRUCacheShard::Release(Cache::Handle* handle, bool force_erase) {
bool last_reference = false; bool last_reference = false;
{ {
MutexLock l(&mutex_); MutexLock l(&mutex_);
last_reference = Unref(e); last_reference = e->Unref();
if (last_reference && e->InCache()) {
// The item is still in cache, and nobody else holds a reference to it
if (usage_ > capacity_ || force_erase) {
// The LRU list must be empty since the cache is full
assert(lru_.next == &lru_ || force_erase);
// Take this opportunity and remove the item
table_.Remove(e->key(), e->hash);
e->SetInCache(false);
} else {
// Put the item back on the LRU list, and don't free it
LRU_Insert(e);
last_reference = false;
}
}
if (last_reference) { if (last_reference) {
usage_ -= e->charge; usage_ -= e->charge;
} }
if (e->refs == 1 && e->InCache()) {
// The item is still in cache, and nobody else holds a reference to it
if (usage_ > capacity_ || force_erase) {
// the cache is full
// The LRU list must be empty since the cache is full
assert(!(usage_ > capacity_) || lru_.next == &lru_);
// take this opportunity and remove the item
table_.Remove(e->key(), e->hash);
e->SetInCache(false);
Unref(e);
usage_ -= e->charge;
last_reference = true;
} else {
// put the item on the list to be potentially freed
LRU_Insert(e);
}
}
} }
// free outside of mutex // Free the entry here outside of mutex for performance reasons
if (last_reference) { if (last_reference) {
e->Free(); e->Free();
} }
@ -342,7 +331,7 @@ Status LRUCacheShard::Insert(const Slice& key, uint32_t hash, void* value,
// It shouldn't happen very often though. // It shouldn't happen very often though.
LRUHandle* e = reinterpret_cast<LRUHandle*>( LRUHandle* e = reinterpret_cast<LRUHandle*>(
new char[sizeof(LRUHandle) - 1 + key.size()]); new char[sizeof(LRUHandle) - 1 + key.size()]);
Status s; Status s = Status::OK();
autovector<LRUHandle*> last_reference_list; autovector<LRUHandle*> last_reference_list;
e->value = value; e->value = value;
@ -351,9 +340,7 @@ Status LRUCacheShard::Insert(const Slice& key, uint32_t hash, void* value,
e->key_length = key.size(); e->key_length = key.size();
e->flags = 0; e->flags = 0;
e->hash = hash; e->hash = hash;
e->refs = (handle == nullptr e->refs = 0;
? 1
: 2); // One from LRUCache, one for the returned handle
e->next = e->prev = nullptr; e->next = e->prev = nullptr;
e->SetInCache(true); e->SetInCache(true);
e->SetPriority(priority); e->SetPriority(priority);
@ -366,11 +353,12 @@ Status LRUCacheShard::Insert(const Slice& key, uint32_t hash, void* value,
// is freed or the lru list is empty // is freed or the lru list is empty
EvictFromLRU(charge, &last_reference_list); EvictFromLRU(charge, &last_reference_list);
if (usage_ - lru_usage_ + charge > capacity_ && if ((usage_ + charge) > capacity_ &&
(strict_capacity_limit_ || handle == nullptr)) { (strict_capacity_limit_ || handle == nullptr)) {
if (handle == nullptr) { if (handle == nullptr) {
// Don't insert the entry but still return ok, as if the entry inserted // Don't insert the entry but still return ok, as if the entry inserted
// into cache and get evicted immediately. // into cache and get evicted immediately.
e->SetInCache(false);
last_reference_list.push_back(e); last_reference_list.push_back(e);
} else { } else {
delete[] reinterpret_cast<char*>(e); delete[] reinterpret_cast<char*>(e);
@ -378,32 +366,30 @@ Status LRUCacheShard::Insert(const Slice& key, uint32_t hash, void* value,
s = Status::Incomplete("Insert failed due to LRU cache being full."); s = Status::Incomplete("Insert failed due to LRU cache being full.");
} }
} else { } else {
// insert into the cache // Insert into the cache. Note that the cache might get larger than its
// note that the cache might get larger than its capacity if not enough // capacity if not enough space was freed up.
// space was freed
LRUHandle* old = table_.Insert(e); LRUHandle* old = table_.Insert(e);
usage_ += e->charge; usage_ += e->charge;
if (old != nullptr) { if (old != nullptr) {
assert(old->InCache());
old->SetInCache(false); old->SetInCache(false);
if (Unref(old)) { if (!old->HasRefs()) {
usage_ -= old->charge; // old is on LRU because it's in cache and its reference count is 0
// old is on LRU because it's in cache and its reference count
// was just 1 (Unref returned 0)
LRU_Remove(old); LRU_Remove(old);
usage_ -= old->charge;
last_reference_list.push_back(old); last_reference_list.push_back(old);
} }
} }
if (handle == nullptr) { if (handle == nullptr) {
LRU_Insert(e); LRU_Insert(e);
} else { } else {
e->Ref();
*handle = reinterpret_cast<Cache::Handle*>(e); *handle = reinterpret_cast<Cache::Handle*>(e);
} }
s = Status::OK();
} }
} }
// we free the entries here outside of mutex for // Free the entries here outside of mutex for performance reasons
// performance reasons
for (auto entry : last_reference_list) { for (auto entry : last_reference_list) {
entry->Free(); entry->Free();
} }
@ -418,18 +404,18 @@ void LRUCacheShard::Erase(const Slice& key, uint32_t hash) {
MutexLock l(&mutex_); MutexLock l(&mutex_);
e = table_.Remove(key, hash); e = table_.Remove(key, hash);
if (e != nullptr) { if (e != nullptr) {
last_reference = Unref(e); assert(e->InCache());
if (last_reference) {
usage_ -= e->charge;
}
if (last_reference && e->InCache()) {
LRU_Remove(e);
}
e->SetInCache(false); e->SetInCache(false);
if (!e->HasRefs()) {
// The entry is in LRU since it's in hash and has no external references
LRU_Remove(e);
usage_ -= e->charge;
last_reference = true;
}
} }
} }
// mutex not held here // Free the entry here outside of mutex for performance reasons
// last_reference will only be true if e != nullptr // last_reference will only be true if e != nullptr
if (last_reference) { if (last_reference) {
e->Free(); e->Free();

76
cache/lru_cache.h vendored
View File

@ -17,31 +17,34 @@
namespace rocksdb { namespace rocksdb {
// LRU cache implementation // LRU cache implementation. This class is not thread-safe.
// An entry is a variable length heap-allocated structure. // An entry is a variable length heap-allocated structure.
// Entries are referenced by cache and/or by any external entity. // Entries are referenced by cache and/or by any external entity.
// The cache keeps all its entries in table. Some elements // The cache keeps all its entries in a hash table. Some elements
// are also stored on LRU list. // are also stored on LRU list.
// //
// LRUHandle can be in these states: // LRUHandle can be in these states:
// 1. Referenced externally AND in hash table. // 1. Referenced externally AND in hash table.
// In that case the entry is *not* in the LRU. (refs > 1 && in_cache == true) // In that case the entry is *not* in the LRU list
// 2. Not referenced externally and in hash table. In that case the entry is // (refs >= 1 && in_cache == true)
// in the LRU and can be freed. (refs == 1 && in_cache == true) // 2. Not referenced externally AND in hash table.
// 3. Referenced externally and not in hash table. In that case the entry is // In that case the entry is in the LRU list and can be freed.
// in not on LRU and not in table. (refs >= 1 && in_cache == false) // (refs == 0 && in_cache == true)
// 3. Referenced externally AND not in hash table.
// In that case the entry is not in the LRU list and not in hash table.
// The entry can be freed when refs becomes 0.
// (refs >= 1 && in_cache == false)
// //
// All newly created LRUHandles are in state 1. If you call // All newly created LRUHandles are in state 1. If you call
// LRUCacheShard::Release // LRUCacheShard::Release on entry in state 1, it will go into state 2.
// on entry in state 1, it will go into state 2. To move from state 1 to // To move from state 1 to state 3, either call LRUCacheShard::Erase or
// state 3, either call LRUCacheShard::Erase or LRUCacheShard::Insert with the // LRUCacheShard::Insert with the same key (but possibly different value).
// same key.
// To move from state 2 to state 1, use LRUCacheShard::Lookup. // To move from state 2 to state 1, use LRUCacheShard::Lookup.
// Before destruction, make sure that no handles are in state 1. This means // Before destruction, make sure that no handles are in state 1. This means
// that any successful LRUCacheShard::Lookup/LRUCacheShard::Insert have a // that any successful LRUCacheShard::Lookup/LRUCacheShard::Insert have a
// matching // matching LRUCache::Release (to move into state 2) or LRUCacheShard::Erase
// RUCache::Release (to move into state 2) or LRUCacheShard::Erase (for state 3) // (to move into state 3).
struct LRUHandle { struct LRUHandle {
void* value; void* value;
@ -51,37 +54,42 @@ struct LRUHandle {
LRUHandle* prev; LRUHandle* prev;
size_t charge; // TODO(opt): Only allow uint32_t? size_t charge; // TODO(opt): Only allow uint32_t?
size_t key_length; size_t key_length;
uint32_t refs; // a number of refs to this entry // The hash of key(). Used for fast sharding and comparisons.
// cache itself is counted as 1 uint32_t hash;
// The number of external refs to this entry. The cache itself is not counted.
uint32_t refs;
// Include the following flags:
// IN_CACHE: whether this entry is referenced by the hash table.
// IS_HIGH_PRI: whether this entry is high priority entry.
// IN_HIGH_PRI_POOL: whether this entry is in high-pri pool.
// HAS_HIT: whether this entry has had any lookups (hits).
enum Flags : uint8_t { enum Flags : uint8_t {
// Whether this entry is referenced by the hash table.
IN_CACHE = (1 << 0), IN_CACHE = (1 << 0),
// Whether this entry is high priority entry.
IS_HIGH_PRI = (1 << 1), IS_HIGH_PRI = (1 << 1),
// Whether this entry is in high-pri pool.
IN_HIGH_PRI_POOL = (1 << 2), IN_HIGH_PRI_POOL = (1 << 2),
// Wwhether this entry has had any lookups (hits).
HAS_HIT = (1 << 3), HAS_HIT = (1 << 3),
}; };
uint8_t flags; uint8_t flags;
uint32_t hash; // Hash of key(); used for fast sharding and comparisons // Beginning of the key (MUST BE THE LAST FIELD IN THIS STRUCT!)
char key_data[1];
char key_data[1]; // Beginning of key Slice key() const { return Slice(key_data, key_length); }
Slice key() const { // Increase the reference count by 1.
// For cheaper lookups, we allow a temporary Handle object void Ref() { refs++; }
// to store a pointer to a key in "value".
if (next == this) { // Just reduce the reference count by 1. Return true if it was last reference.
return *(reinterpret_cast<Slice*>(value)); bool Unref() {
} else { assert(refs > 0);
return Slice(key_data, key_length); refs--;
} return refs == 0;
} }
// Return true if there are external refs, false otherwise.
bool HasRefs() const { return refs > 0; }
bool InCache() const { return flags & IN_CACHE; } bool InCache() const { return flags & IN_CACHE; }
bool IsHighPri() const { return flags & IS_HIGH_PRI; } bool IsHighPri() const { return flags & IS_HIGH_PRI; }
bool InHighPriPool() const { return flags & IN_HIGH_PRI_POOL; } bool InHighPriPool() const { return flags & IN_HIGH_PRI_POOL; }
@ -114,7 +122,7 @@ struct LRUHandle {
void SetHit() { flags |= HAS_HIT; } void SetHit() { flags |= HAS_HIT; }
void Free() { void Free() {
assert((refs == 1 && InCache()) || (refs == 0 && !InCache())); assert(refs == 0);
if (deleter) { if (deleter) {
(*deleter)(key(), value); (*deleter)(key(), value);
} }
@ -169,7 +177,7 @@ class ALIGN_AS(CACHE_LINE_SIZE) LRUCacheShard final : public CacheShard {
public: public:
LRUCacheShard(size_t capacity, bool strict_capacity_limit, LRUCacheShard(size_t capacity, bool strict_capacity_limit,
double high_pri_pool_ratio, bool use_adaptive_mutex); double high_pri_pool_ratio, bool use_adaptive_mutex);
virtual ~LRUCacheShard(); virtual ~LRUCacheShard() override = default;
// Separate from constructor so caller can easily make an array of LRUCache // Separate from constructor so caller can easily make an array of LRUCache
// if current usage is more than new capacity, the function will attempt to // if current usage is more than new capacity, the function will attempt to
@ -225,10 +233,6 @@ class ALIGN_AS(CACHE_LINE_SIZE) LRUCacheShard final : public CacheShard {
// high-pri pool is no larger than the size specify by high_pri_pool_pct. // high-pri pool is no larger than the size specify by high_pri_pool_pct.
void MaintainPoolSize(); void MaintainPoolSize();
// Just reduce the reference count by 1.
// Return true if last reference
bool Unref(LRUHandle* e);
// Free some space following strict LRU policy until enough space // Free some space following strict LRU policy until enough space
// to hold (usage_ + charge) is freed or the lru list is empty // to hold (usage_ + charge) is freed or the lru list is empty
// This function is not thread safe - it needs to be executed while // This function is not thread safe - it needs to be executed while