mirror of
https://github.com/Zygo/bees.git
synced 2025-05-17 21:35: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:
parent
b1bdd9e056
commit
6ee5da7d77
@ -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));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user