1
0
mirror of https://github.com/Zygo/bees.git synced 2025-05-17 13:25:45 +02:00

cache: clean up pointer mangling and duplicate code

std::list and std::map both have stable iterators, and list has the
splice() method, so we don't need a hand-rolled double-linked list here.

Coalesce insert() and operator() into a single function.

Drop the unused prune() method.

Move destructor calls for cached objects out from under the cache lock.
Closing a lot of files at once is already expensive, might as well not
stop the world while we do it.

Signed-off-by: Zygo Blaxell <bees@furryterror.org>
This commit is contained in:
Zygo Blaxell 2020-10-18 17:40:58 -04:00
parent b1bdd9e056
commit 6ee5da7d77

View File

@ -5,6 +5,7 @@
#include <algorithm> #include <algorithm>
#include <functional> #include <functional>
#include <list>
#include <map> #include <map>
#include <mutex> #include <mutex>
#include <tuple> #include <tuple>
@ -20,25 +21,24 @@ namespace crucible {
using Func = function<Return(Arguments...)>; using Func = function<Return(Arguments...)>;
private: private:
struct Value { struct Value {
Value *fp = nullptr;
Value *bp = nullptr;
Key key; Key key;
Return ret; Return ret;
Value(Key k, Return r) : key(k), ret(r) { }
// Crash early!
~Value() { fp = bp = nullptr; };
}; };
Func m_fn; using ListIter = typename list<Value>::iterator;
map<Key, Value> m_map;
LockSet<Key> m_lockset; Func m_fn;
size_t m_max_size; list<Value> m_list;
mutex m_mutex; map<Key, ListIter> m_map;
Value *m_last = nullptr; LockSet<Key> m_lockset;
size_t m_max_size;
mutex m_mutex;
void check_overflow(); void check_overflow();
void move_to_front(Value *vp); void recent_use(ListIter vp);
void erase_one(Value *vp); void erase_item(ListIter vp);
void erase_key(const Key &k);
Return insert_item(Func fn, Arguments... args);
public: public:
LRUCache(Func f = Func(), size_t max_size = 100); LRUCache(Func f = Func(), size_t max_size = 100);
@ -48,7 +48,6 @@ namespace crucible {
Return operator()(Arguments... args); Return operator()(Arguments... args);
Return refresh(Arguments... args); Return refresh(Arguments... args);
void expire(Arguments... args); void expire(Arguments... args);
void prune(function<bool(const Return &)> predicate);
void insert(const Return &r, Arguments... args); void insert(const Return &r, Arguments... args);
void clear(); void clear();
}; };
@ -61,30 +60,81 @@ namespace crucible {
} }
template <class Return, class... Arguments> template <class Return, class... Arguments>
void Return
LRUCache<Return, Arguments...>::erase_one(Value *vp) LRUCache<Return, Arguments...>::insert_item(Func fn, Arguments... args)
{ {
THROW_CHECK0(invalid_argument, vp); Key k(args...);
Value *vp_bp = vp->bp;
THROW_CHECK0(runtime_error, vp_bp); // Do we have it cached?
Value *vp_fp = vp->fp; unique_lock<mutex> lock(m_mutex);
THROW_CHECK0(runtime_error, vp_fp); auto found = m_map.find(k);
vp_fp->bp = vp_bp; if (found == m_map.end()) {
vp_bp->fp = vp_fp; // No, release cache lock and acquire key lock
// If we delete the head of the list then advance the head by one lock.unlock();
if (vp == m_last) { auto key_lock = m_lockset.make_lock(k);
// If the head of the list is also the tail of the list then clear m_last
if (vp_fp == m_last) { // Did item appear in cache while we were waiting for key?
m_last = nullptr; lock.lock();
} else { found = m_map.find(k);
m_last = vp_fp; if (found == m_map.end()) {
// No, we now hold key and cache locks, but item not in cache.
// Release cache lock and call the function
lock.unlock();
// Create new value
Value v {
.key = k,
.ret = fn(args...),
};
// Reacquire cache lock
lock.lock();
// Make room
check_overflow();
// Insert return value at back of LRU list (hot end)
auto new_item = m_list.insert(m_list.end(), v);
// Insert return value in map
bool inserted = false;
tie(found, inserted) = m_map.insert(make_pair(v.key, new_item));
// We (should be) holding a lock on this key so we are the ones to insert it
THROW_CHECK0(runtime_error, inserted);
} }
}
m_map.erase(vp->key); // Item should be in cache now
if (!m_last) { THROW_CHECK0(runtime_error, found != m_map.end());
THROW_CHECK0(runtime_error, m_map.empty());
} else { } else {
THROW_CHECK0(runtime_error, !m_map.empty()); // Move to end of LRU
recent_use(found->second);
}
// Return cached object
return found->second->ret;
}
template <class Return, class... Arguments>
void
LRUCache<Return, Arguments...>::erase_item(ListIter vp)
{
if (vp != m_list.end()) {
m_map.erase(vp->key);
m_list.erase(vp);
}
}
template <class Return, class... Arguments>
void
LRUCache<Return, Arguments...>::erase_key(const Key &k)
{
auto map_item = m_map.find(k);
if (map_item != m_map.end()) {
auto list_item = map_item->second;
m_map.erase(map_item);
m_list.erase(list_item);
} }
} }
@ -92,46 +142,20 @@ namespace crucible {
void void
LRUCache<Return, Arguments...>::check_overflow() LRUCache<Return, Arguments...>::check_overflow()
{ {
while (m_map.size() >= m_max_size) { // Erase items at front of LRU list (cold end) until max size reached or list empty
THROW_CHECK0(runtime_error, m_last); while (m_map.size() >= m_max_size && !m_list.empty()) {
THROW_CHECK0(runtime_error, m_last->bp); erase_item(m_list.begin());
erase_one(m_last->bp);
} }
} }
template <class Return, class... Arguments> template <class Return, class... Arguments>
void void
LRUCache<Return, Arguments...>::move_to_front(Value *vp) LRUCache<Return, Arguments...>::recent_use(ListIter vp)
{ {
if (!m_last) { // Splice existing items at back of LRU list (hot end)
// Create new LRU list auto next_vp = vp;
m_last = vp->fp = vp->bp = vp; ++next_vp;
} else if (m_last != vp) { m_list.splice(m_list.end(), m_list, vp, next_vp);
Value *vp_fp = vp->fp;
Value *vp_bp = vp->bp;
if (vp_fp && vp_bp) {
// There are at least two and we are removing one that isn't m_last
// Connect adjacent nodes to each other (has no effect if vp is new), removing vp from list
vp_fp->bp = vp_bp;
vp_bp->fp = vp_fp;
} else {
// New insertion, both must be null
THROW_CHECK0(runtime_error, !vp_fp);
THROW_CHECK0(runtime_error, !vp_bp);
}
// Splice new node into list
Value *last_bp = m_last->bp;
THROW_CHECK0(runtime_error, last_bp);
// New element points to both ends of list
vp->fp = m_last;
vp->bp = last_bp;
// Insert vp as fp from the end of the list
last_bp->fp = vp;
// Insert vp as bp from the second from the start of the list
m_last->bp = vp;
// Update start of list
m_last = vp;
}
} }
template <class Return, class... Arguments> template <class Return, class... Arguments>
@ -158,93 +182,29 @@ namespace crucible {
void void
LRUCache<Return, Arguments...>::clear() LRUCache<Return, Arguments...>::clear()
{ {
// Move the map onto the stack, then destroy it after we've released the lock. // Move the map and list onto the stack, then destroy it after we've released the lock
// so that we don't block other threads if the list's destructors are expensive
decltype(m_list) new_list;
decltype(m_map) new_map; decltype(m_map) new_map;
unique_lock<mutex> lock(m_mutex); unique_lock<mutex> lock(m_mutex);
m_list.swap(new_list);
m_map.swap(new_map); m_map.swap(new_map);
m_last = nullptr; lock.unlock();
}
template <class Return, class... Arguments>
void
LRUCache<Return, Arguments...>::prune(function<bool(const Return &)> pred)
{
unique_lock<mutex> lock(m_mutex);
for (auto it = m_map.begin(); it != m_map.end(); ) {
auto next_it = ++it;
if (pred(it.second.ret)) {
erase_one(&it.second);
}
it = next_it;
}
} }
template<class Return, class... Arguments> template<class Return, class... Arguments>
Return Return
LRUCache<Return, Arguments...>::operator()(Arguments... args) LRUCache<Return, Arguments...>::operator()(Arguments... args)
{ {
Key k(args...); return insert_item(m_fn, args...);
bool inserted = false;
// Do we have it cached?
unique_lock<mutex> lock(m_mutex);
auto found = m_map.find(k);
if (found == m_map.end()) {
// No, release cache lock and acquire key lock
lock.unlock();
auto key_lock = m_lockset.make_lock(k);
// Did item appear in cache while we were waiting for key?
lock.lock();
found = m_map.find(k);
if (found == m_map.end()) {
// No, we hold key and cache locks, but item not in cache.
// Release cache lock and call function
lock.unlock();
// Create new value
Value v(k, m_fn(args...));
// Reacquire cache lock
lock.lock();
// Make room
check_overflow();
// Reacquire cache lock and insert return value
tie(found, inserted) = m_map.insert(make_pair(k, v));
// We hold a lock on this key so we are the ones to insert it
THROW_CHECK0(runtime_error, inserted);
// Release key lock, keep the cache lock
key_lock.unlock();
}
}
// Item should be in cache now
THROW_CHECK0(runtime_error, found != m_map.end());
// (Re)insert at head of LRU
move_to_front(&(found->second));
// Make copy before releasing lock
auto rv = found->second.ret;
return rv;
} }
template<class Return, class... Arguments> template<class Return, class... Arguments>
void void
LRUCache<Return, Arguments...>::expire(Arguments... args) LRUCache<Return, Arguments...>::expire(Arguments... args)
{ {
Key k(args...);
unique_lock<mutex> lock(m_mutex); unique_lock<mutex> lock(m_mutex);
auto found = m_map.find(k); erase_key(Key(args...));
if (found != m_map.end()) {
erase_one(&found->second);
}
} }
template<class Return, class... Arguments> template<class Return, class... Arguments>
@ -259,40 +219,7 @@ namespace crucible {
void void
LRUCache<Return, Arguments...>::insert(const Return &r, Arguments... args) LRUCache<Return, Arguments...>::insert(const Return &r, Arguments... args)
{ {
Key k(args...); insert_item([&](Arguments...) -> Return { return r; }, args...);
bool inserted = false;
// Do we have it cached?
unique_lock<mutex> lock(m_mutex);
auto found = m_map.find(k);
if (found == m_map.end()) {
// No, release cache lock and acquire key lock
lock.unlock();
auto key_lock = m_lockset.make_lock(k);
// Did item appear in cache while we were waiting for key?
lock.lock();
found = m_map.find(k);
if (found == m_map.end()) {
// Make room
check_overflow();
// No, we hold key and cache locks, but item not in cache.
// Insert the provided return value (no need to unlock here)
Value v(k, r);
tie(found, inserted) = m_map.insert(make_pair(k, v));
// We hold a lock on this key so we are the ones to insert it
THROW_CHECK0(runtime_error, inserted);
}
}
// Item should be in cache now
THROW_CHECK0(runtime_error, found != m_map.end());
// (Re)insert at head of LRU
move_to_front(&(found->second));
} }
} }