genode/repos/gems/include/gems/lru_cache.h
2021-12-17 15:04:46 +01:00

308 lines
6.7 KiB
C++

/*
* \brief Cache with a least-recently-used eviction policy
* \author Norman Feske
* \date 2019-01-12
*/
/*
* Copyright (C) 2019 Genode Labs GmbH
*
* This file is part of the Genode OS framework, which is distributed
* under the terms of the GNU Affero General Public License version 3.
*/
#ifndef _INCLUDE__GEMS__LRU_CACHE_H_
#define _INCLUDE__GEMS__LRU_CACHE_H_
#include <base/output.h>
#include <base/allocator.h>
namespace Genode { template <typename, typename> class Lru_cache; }
template <typename KEY, typename ELEM>
class Genode::Lru_cache : Noncopyable
{
public:
struct Stats
{
unsigned hits, evictions;
void print(Output &out) const
{
Genode::print(out, "hits: ", hits, ", evictions: ", evictions);
}
};
private:
struct Time { unsigned value; };
Allocator &_alloc;
size_t const _max_elements;
size_t _used_elements = 0;
Time _now { 0 };
Stats _stats { };
class Tag
{
protected:
KEY const _key;
Time _last_used;
Tag(KEY const &key, Time now) : _key(key), _last_used(now) { }
};
/*
* The '_key' and '_last_used' attributes are supplemented as the 'Tag'
* base class to the 'Element'instead of being 'Element' member
* variables to allow 'ELEM' to be at the trailing end of the object.
* This way, 'ELEM' can be a variable-length type (using a flexible
* array member).
*/
class Element : private Tag, private Avl_node<Element>, public ELEM
{
private:
friend class Avl_node<Element>;
friend class Avl_tree<Element>;
friend class Lru_cache;
bool _higher(KEY const &other) const
{
return Tag::_key.value > other.value;
}
unsigned _importance(Time now) const
{
return now.value - Tag::_last_used.value;
}
public:
template <typename... ARGS>
Element(KEY key, Time now, ARGS &&... args)
: Tag(key, now), ELEM(args...) { }
/**
* Avl_node interface
*/
bool higher(Element *e) { return _higher(e->_key); }
template <typename FN>
bool try_apply(KEY const &key, FN const &fn)
{
if (Tag::_key.value == key.value) {
fn(*this);
return true;
}
Element * const e = Avl_node<Element>::child(_higher(key));
return e && e->try_apply(key, fn);
}
template <typename FN>
void with_least_recently_used(Time now, FN const fn)
{
Element *result = this;
for (unsigned i = 0; i < 2; i++) {
Element *e = Avl_node<Element>::child(i);
if (e && e->_importance(now) > result->_importance(now))
result = e;
}
if (result)
fn(*result);
}
void mark_as_used(Time now) { Tag::_last_used = now; }
};
Avl_tree<Element> mutable _avl_tree { };
/**
* Add cache entry for the given key
*
* \param ARGS constructor arguments passed to new ELEM
*
* \throw Out_of_ram
* \throw Out_of_caps
*/
template <typename... ARGS>
void _insert(KEY key, ARGS &... args)
{
auto const element_ptr = (Element *)_alloc.alloc(sizeof(Element));
_used_elements++;
construct_at<Element>(element_ptr, key, _now, args...);
_avl_tree.insert(element_ptr);
}
/**
* Evict element from cache
*/
void _remove(Element &element)
{
_avl_tree.remove(&element);
element.~Element();
_alloc.free(&element, sizeof(Element));
_used_elements--;
_stats.evictions++;
}
template <typename FN>
bool _try_apply(KEY const &key, FN const &fn)
{
if (!_avl_tree.first())
return false;
return _avl_tree.first()->try_apply(key, fn);
}
/**
* Evict least recently used element from cache
*
* \return true if an element was released
*/
bool _remove_least_recently_used()
{
if (!_avl_tree.first())
return false;
_avl_tree.first()->with_least_recently_used(_now, [&] (Element &e) {
_remove(e); });
return true;
}
void _remove_all()
{
while (Element *element_ptr = _avl_tree.first())
_remove(*element_ptr);
}
public:
struct Size { size_t value; };
/**
* Constructor
*
* \param alloc backing store for cache elements
* \param size maximum number of cache elements
*/
Lru_cache(Allocator &alloc, Size size)
: _alloc(alloc), _max_elements(size.value) { }
~Lru_cache() { _remove_all(); }
/**
* Return size of a single cache entry including the meta data
*
* The returned value is useful for cache-dimensioning calculations.
*/
static constexpr size_t element_size() { return sizeof(Element); }
/**
* Return usage stats
*/
Stats stats() const { return _stats; }
/**
* Interface presented to the cache-miss handler to construct an
* element
*/
class Missing_element : Noncopyable
{
private:
friend class Lru_cache;
Lru_cache &_cache;
KEY const &_key;
Missing_element(Lru_cache &cache, KEY const &key)
: _cache(cache), _key(key) { }
public:
/**
* Populate cache with new element
*
* \param ARGS arguments passed to the 'ELEM' constructor
*/
template <typename... ARGS>
void construct(ARGS &... args) { _cache._insert(_key, args...); }
};
/**
* Apply functor 'hit_fn' to element with matching 'key'
*
* \param miss_fn cache-miss handler
*
* \return true if 'hit_fn' got executed
*
* If the requested 'key' is not present in the cache, the cache-miss
* handler is called with the interface 'Missing_element &' as
* argument. By calling 'Missing_element::construct', the handler is
* able to fill the cache with the missing element. After resolving a
* cache miss, 'hit_fn' is called for the freshly inserted element.
*
* If an occurring cache miss is not handled, 'hit_fn' is not called.
*/
template <typename HIT_FN, typename MISS_FN>
bool try_apply(KEY key, HIT_FN const &hit_fn, MISS_FN const &miss_fn)
{
_now.value++;
/*
* Try to look up element from the cache. If it is missing, fill
* cache with requested element and repeat the lookup. When under
* memory pressure, evict the least recently used element from the
* cache.
*/
/* retry once after handling a cache miss */
for (unsigned i = 0; i < 2; i++) {
bool const hit = _try_apply(key, [&] (Element &element) {
hit_fn(element);
element.mark_as_used(_now);
_stats.hits += (i == 0);
});
if (hit)
return true;
/*
* Handle cache miss
*/
/* evict element if the cache is fully populated */
while (_used_elements >= _max_elements)
if (!_remove_least_recently_used())
break;
/* fetch missing element into the cache */
Missing_element missing_element { *this, key };
miss_fn(missing_element);
}
return false;
}
};
#endif /* _INCLUDE__GEMS__LRU_CACHE_H_ */