forked from mirrors/0ad
Remove generic cache
The removal of the VFS cache in 89e339dd16
meant there is no longer a user. As such also clean out the generic
parts with the Landlord and a LRU policies.
Signed-off-by: Ralph Sennhauser <ralph.sennhauser@gmail.com>
This commit is contained in:
@@ -1,744 +0,0 @@
|
||||
/* Copyright (C) 2025 Wildfire Games.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining
|
||||
* a copy of this software and associated documentation files (the
|
||||
* "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||
* permit persons to whom the Software is furnished to do so, subject to
|
||||
* the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included
|
||||
* in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Customizable cache data structure.
|
||||
*/
|
||||
|
||||
#ifndef INCLUDED_CACHE_ADT
|
||||
#define INCLUDED_CACHE_ADT
|
||||
|
||||
#include "lib/debug.h"
|
||||
#include "lib/status.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cfloat>
|
||||
#include <cstddef>
|
||||
#include <list>
|
||||
#include <queue> // std::priority_queue
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
/*
|
||||
Cache for items of variable size and value/"cost".
|
||||
underlying displacement algorithm is pluggable; default is "Landlord".
|
||||
|
||||
template reference:
|
||||
Entry provides size, cost, credit and credit_density().
|
||||
rationale:
|
||||
- made a template instead of exposing Cache::Entry because
|
||||
that would drag a lot of stuff out of Cache.
|
||||
- calculates its own density since that entails a Divider functor,
|
||||
which requires storage inside Entry.
|
||||
Entries is a collection with iterator and begin()/end() and
|
||||
"static Entry& entry_from_it(iterator)".
|
||||
rationale:
|
||||
- STL map has pair<key, item> as its value_type, so this
|
||||
function would return it->second. however, we want to support
|
||||
other container types (where we'd just return *it).
|
||||
Manager is a template parameterized on typename Key and class Entry.
|
||||
its interface is as follows:
|
||||
|
||||
// is the cache empty?
|
||||
bool empty() const;
|
||||
|
||||
// add (key, entry) to cache.
|
||||
void add(const Key& key, const Entry& entry);
|
||||
|
||||
// if the entry identified by <key> is not in cache, return false;
|
||||
// otherwise return true and pass back a pointer to it.
|
||||
bool find(const Key& key, const Entry** pentry) const;
|
||||
|
||||
// remove an entry from cache, which is assumed to exist!
|
||||
// this makes sense because callers will typically first use find() to
|
||||
// return info about the entry; this also checks if present.
|
||||
void remove(const Key& key);
|
||||
|
||||
// mark <entry> as just accessed for purpose of cache management.
|
||||
// it will tend to be kept in cache longer.
|
||||
void on_access(Entry& entry);
|
||||
|
||||
// caller's intent is to remove the least valuable entry.
|
||||
// in implementing this, you have the latitude to "shake loose"
|
||||
// several entries (e.g. because their 'value' is equal).
|
||||
// they must all be push_back-ed into the list; Cache will dole
|
||||
// them out one at a time in FIFO order to callers.
|
||||
//
|
||||
// rationale:
|
||||
// - it is necessary for callers to receive a copy of the
|
||||
// Entry being evicted - e.g. file_cache owns its items and
|
||||
// they must be passed back to allocator when evicted.
|
||||
// - e.g. Landlord can potentially see several entries become
|
||||
// evictable in one call to remove_least_valuable. there are
|
||||
// several ways to deal with this:
|
||||
// 1) generator interface: we return one of { empty, nevermind,
|
||||
// removed, remove-and-call-again }. this greatly complicates
|
||||
// the call site.
|
||||
// 2) return immediately after finding an item to evict.
|
||||
// this changes cache behavior - entries stored at the
|
||||
// beginning would be charged more often (unfair).
|
||||
// resuming charging at the next entry doesn't work - this
|
||||
// would have to be flushed when adding, at which time there
|
||||
// is no provision for returning any items that may be evicted.
|
||||
// 3) return list of all entries "shaken loose". this incurs
|
||||
// frequent mem allocs, which can be alleviated via suballocator.
|
||||
// note: an intrusive linked-list doesn't make sense because
|
||||
// entries to be returned need to be copied anyway (they are
|
||||
// removed from the manager's storage).
|
||||
void remove_least_valuable(std::list<Entry>& entry_list)
|
||||
*/
|
||||
|
||||
|
||||
//
|
||||
// functors to calculate minimum credit density (MCD)
|
||||
//
|
||||
|
||||
// MCD is required for the Landlord algorithm's evict logic.
|
||||
// [Young02] calls it '\delta'.
|
||||
|
||||
// scan over all entries and return MCD.
|
||||
template<class Entries> float ll_calc_min_credit_density(const Entries& entries)
|
||||
{
|
||||
float min_credit_density = FLT_MAX;
|
||||
for(typename Entries::const_iterator it = entries.begin(); it != entries.end(); ++it)
|
||||
{
|
||||
const float credit_density = Entries::entry_from_it(it).credit_density();
|
||||
min_credit_density = std::min(min_credit_density, credit_density);
|
||||
}
|
||||
return min_credit_density;
|
||||
}
|
||||
|
||||
// note: no warning is given that the MCD entry is being removed!
|
||||
// (reduces overhead in remove_least_valuable)
|
||||
// these functors must account for that themselves (e.g. by resetting
|
||||
// their state directly after returning MCD).
|
||||
|
||||
// determine MCD by scanning over all entries.
|
||||
// tradeoff: O(N) time complexity, but all notify* calls are no-ops.
|
||||
template<class Entry, class Entries>
|
||||
class McdCalc_Naive
|
||||
{
|
||||
public:
|
||||
void notify_added(const Entry&) const {}
|
||||
void notify_decreased(const Entry&) const {}
|
||||
void notify_impending_increase_or_remove(const Entry&) const {}
|
||||
void notify_increased_or_removed(const Entry&) const {}
|
||||
float operator()(const Entries& entries) const
|
||||
{
|
||||
const float mcd = ll_calc_min_credit_density(entries);
|
||||
return mcd;
|
||||
}
|
||||
};
|
||||
|
||||
// cache previous MCD and update it incrementally (when possible).
|
||||
// tradeoff: amortized O(1) time complexity, but notify* calls must
|
||||
// perform work whenever something in the cache changes.
|
||||
template<class Entry, class Entries>
|
||||
class McdCalc_Cached
|
||||
{
|
||||
public:
|
||||
McdCalc_Cached() : min_credit_density(FLT_MAX), min_valid(false) {}
|
||||
|
||||
void notify_added(const Entry& entry)
|
||||
{
|
||||
// when adding a new item, the minimum credit density can only
|
||||
// decrease or remain the same; acting as if entry's credit had
|
||||
// been decreased covers both cases.
|
||||
notify_decreased(entry);
|
||||
}
|
||||
|
||||
void notify_decreased(const Entry& entry)
|
||||
{
|
||||
min_credit_density = std::min(min_credit_density, entry.credit_density());
|
||||
}
|
||||
|
||||
void notify_impending_increase_or_remove(const Entry& entry)
|
||||
{
|
||||
// remember if this entry had the smallest density
|
||||
is_min_entry = feq(min_credit_density, entry.credit_density());
|
||||
}
|
||||
|
||||
void notify_increased_or_removed(const Entry&)
|
||||
{
|
||||
// .. it did and was increased or removed. we must invalidate
|
||||
// MCD and recalculate it next time.
|
||||
if(is_min_entry)
|
||||
{
|
||||
min_valid = false;
|
||||
min_credit_density = -1.0f;
|
||||
}
|
||||
}
|
||||
|
||||
float operator()(const Entries& entries)
|
||||
{
|
||||
if(min_valid)
|
||||
{
|
||||
// the entry that has MCD will be removed anyway by caller;
|
||||
// we need to invalidate here because they don't call
|
||||
// notify_increased_or_removed.
|
||||
min_valid = false;
|
||||
return min_credit_density;
|
||||
}
|
||||
|
||||
// this is somewhat counterintuitive. since we're calculating
|
||||
// MCD directly, why not mark our cached version of it valid
|
||||
// afterwards? reason is that our caller will remove the entry with
|
||||
// MCD, so it'll be invalidated anyway.
|
||||
// instead, our intent is to calculate MCD for the *next time*.
|
||||
const float ret = ll_calc_min_credit_density(entries);
|
||||
min_valid = true;
|
||||
min_credit_density = FLT_MAX;
|
||||
return ret;
|
||||
}
|
||||
|
||||
private:
|
||||
float min_credit_density;
|
||||
bool min_valid;
|
||||
|
||||
// temporary flag set by notify_impending_increase_or_remove
|
||||
bool is_min_entry;
|
||||
};
|
||||
|
||||
|
||||
//
|
||||
// Landlord cache management policy: see [Young02].
|
||||
//
|
||||
|
||||
// in short, each entry has credit initially set to cost. when wanting to
|
||||
// remove an item, all are charged according to MCD and their size;
|
||||
// entries are evicted if their credit is exhausted. accessing an entry
|
||||
// restores "some" of its credit.
|
||||
template<typename Key, typename Entry, template<class Entry_, class Entries> class McdCalc = McdCalc_Cached>
|
||||
class Landlord
|
||||
{
|
||||
public:
|
||||
bool empty() const
|
||||
{
|
||||
return map.empty();
|
||||
}
|
||||
|
||||
void add(const Key& key, const Entry& entry)
|
||||
{
|
||||
// adapter for add_ (which returns an iterator)
|
||||
(void)add_(key, entry);
|
||||
}
|
||||
|
||||
bool find(const Key& key, const Entry** pentry) const
|
||||
{
|
||||
MapCIt it = map.find(key);
|
||||
if(it == map.end())
|
||||
return false;
|
||||
*pentry = &it->second;
|
||||
return true;
|
||||
}
|
||||
|
||||
void remove(const Key& key)
|
||||
{
|
||||
MapIt it = map.find(key);
|
||||
// note: don't complain if not in the cache: this happens after
|
||||
// writing a file and invalidating its cache entry, which may
|
||||
// or may not exist.
|
||||
if(it != map.end())
|
||||
remove_(it);
|
||||
}
|
||||
|
||||
void on_access(Entry& entry)
|
||||
{
|
||||
mcd_calc.notify_impending_increase_or_remove(entry);
|
||||
|
||||
// Landlord algorithm calls for credit to be reset to anything
|
||||
// between its current value and the cost.
|
||||
const float gain = 0.75f; // restore most credit
|
||||
entry.credit = gain*entry.cost + (1.0f-gain)*entry.credit;
|
||||
|
||||
mcd_calc.notify_increased_or_removed(entry);
|
||||
}
|
||||
|
||||
void remove_least_valuable(std::list<Entry>& entry_list)
|
||||
{
|
||||
// we are required to evict at least one entry. one iteration
|
||||
// ought to suffice, due to definition of min_credit_density and
|
||||
// tolerance; however, we provide for repeating if necessary.
|
||||
again:
|
||||
|
||||
// messing with this (e.g. raising if tiny) would result in
|
||||
// different evictions than Landlord_Lazy, which is unacceptable.
|
||||
// nor is doing so necessary: if mcd is tiny, so is credit.
|
||||
const float min_credit_density = mcd_calc(map);
|
||||
ENSURE(min_credit_density > 0.0f);
|
||||
|
||||
for(MapIt it = map.begin(); it != map.end();) // no ++it
|
||||
{
|
||||
Entry& entry = it->second;
|
||||
|
||||
charge(entry, min_credit_density);
|
||||
if(should_evict(entry))
|
||||
{
|
||||
entry_list.push_back(entry);
|
||||
|
||||
// annoying: we have to increment <it> before erasing
|
||||
MapIt it_to_remove = it++;
|
||||
map.erase(it_to_remove);
|
||||
}
|
||||
else
|
||||
{
|
||||
mcd_calc.notify_decreased(entry);
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
if(entry_list.empty())
|
||||
goto again;
|
||||
}
|
||||
|
||||
protected:
|
||||
class Map : public std::unordered_map<Key, Entry>
|
||||
{
|
||||
public:
|
||||
static Entry& entry_from_it(typename Map::iterator it) { return it->second; }
|
||||
static const Entry& entry_from_it(typename Map::const_iterator it) { return it->second; }
|
||||
};
|
||||
typedef typename Map::iterator MapIt;
|
||||
typedef typename Map::const_iterator MapCIt;
|
||||
Map map;
|
||||
|
||||
// add entry and return iterator pointing to it.
|
||||
MapIt add_(const Key& key, const Entry& entry)
|
||||
{
|
||||
typedef std::pair<MapIt, bool> PairIB;
|
||||
typename Map::value_type val = std::make_pair(key, entry);
|
||||
PairIB ret = map.insert(val);
|
||||
ENSURE(ret.second); // must not already be in map
|
||||
|
||||
mcd_calc.notify_added(entry);
|
||||
|
||||
return ret.first;
|
||||
}
|
||||
|
||||
// remove entry (given by iterator) directly.
|
||||
void remove_(MapIt it)
|
||||
{
|
||||
const Entry& entry = it->second;
|
||||
mcd_calc.notify_impending_increase_or_remove(entry);
|
||||
mcd_calc.notify_increased_or_removed(entry);
|
||||
map.erase(it);
|
||||
}
|
||||
|
||||
void charge(Entry& entry, float delta)
|
||||
{
|
||||
entry.credit -= delta * entry.size;
|
||||
|
||||
// don't worry about entry.size being 0 - if so, cost
|
||||
// should also be 0, so credit will already be 0 anyway.
|
||||
}
|
||||
|
||||
// for each entry, 'charge' it (i.e. reduce credit by) delta * its size.
|
||||
// delta is typically MCD (see above); however, several such updates
|
||||
// may be lumped together to save time. Landlord_Lazy does this.
|
||||
void charge_all(float delta)
|
||||
{
|
||||
for(MapIt it = map.begin(); it != map.end(); ++it)
|
||||
{
|
||||
Entry& entry = it->second;
|
||||
entry.credit -= delta * entry.size;
|
||||
if(!should_evict(entry))
|
||||
mcd_calc.notify_decreased(entry);
|
||||
}
|
||||
}
|
||||
|
||||
// is entry's credit exhausted? if so, it should be evicted.
|
||||
bool should_evict(const Entry& entry)
|
||||
{
|
||||
// we need a bit of leeway because density calculations may not
|
||||
// be exact. choose value carefully: must not be high enough to
|
||||
// trigger false positives.
|
||||
return entry.credit < 0.0001f;
|
||||
}
|
||||
|
||||
private:
|
||||
McdCalc<Entry, Map> mcd_calc;
|
||||
};
|
||||
|
||||
// Cache manger policies. (these are partial specializations of Landlord,
|
||||
// adapting it to the template params required by Cache)
|
||||
template<class Key, class Entry> class Landlord_Naive : public Landlord<Key, Entry, McdCalc_Naive> {};
|
||||
template<class Key, class Entry> class Landlord_Cached: public Landlord<Key, Entry, McdCalc_Cached> {};
|
||||
|
||||
// variant of Landlord that adds a priority queue to directly determine
|
||||
// which entry to evict. this allows lumping several charge operations
|
||||
// together and thus reduces iteration over all entries.
|
||||
// tradeoff: O(logN) removal (instead of N), but additional O(N) storage.
|
||||
template<typename Key, class Entry>
|
||||
class Landlord_Lazy : public Landlord_Naive<Key, Entry>
|
||||
{
|
||||
typedef typename Landlord_Naive<Key, Entry>::Map Map;
|
||||
typedef typename Landlord_Naive<Key, Entry>::MapIt MapIt;
|
||||
typedef typename Landlord_Naive<Key, Entry>::MapCIt MapCIt;
|
||||
|
||||
public:
|
||||
Landlord_Lazy() { pending_delta = 0.0f; }
|
||||
|
||||
void add(const Key& key, const Entry& entry)
|
||||
{
|
||||
// we must apply pending_delta now - otherwise, the existing delta
|
||||
// would later be applied to this newly added item (incorrect).
|
||||
commit_pending_delta();
|
||||
|
||||
MapIt it = Parent::add_(key, entry);
|
||||
pri_q.push(it);
|
||||
}
|
||||
|
||||
void remove(const Key& key)
|
||||
{
|
||||
Parent::remove(key);
|
||||
|
||||
// reconstruct pri_q from current map. this is slow (N*logN) and
|
||||
// could definitely be done better, but we don't bother since
|
||||
// remove is a very rare operation (e.g. invalidating entries).
|
||||
while(!pri_q.empty())
|
||||
pri_q.pop();
|
||||
for(MapCIt it = this->map.begin(); it != this->map.end(); ++it)
|
||||
pri_q.push(it);
|
||||
}
|
||||
|
||||
void on_access(Entry& entry)
|
||||
{
|
||||
Parent::on_access(entry);
|
||||
|
||||
// entry's credit was changed. we now need to reshuffle the
|
||||
// pri queue to reflect this.
|
||||
pri_q.ensure_heap_order();
|
||||
}
|
||||
|
||||
void remove_least_valuable(std::list<Entry>& entry_list)
|
||||
{
|
||||
MapIt least_valuable_it = pri_q.top(); pri_q.pop();
|
||||
Entry& entry = Map::entry_from_it(least_valuable_it);
|
||||
|
||||
entry_list.push_back(entry);
|
||||
|
||||
// add to pending_delta the MCD that would have resulted
|
||||
// if removing least_valuable_it normally.
|
||||
// first, calculate actual credit (i.e. apply pending_delta to
|
||||
// this entry); then add the resulting density to pending_delta.
|
||||
entry.credit -= pending_delta*entry.size;
|
||||
const float credit_density = entry.credit_density();
|
||||
ENSURE(credit_density > 0.0f);
|
||||
pending_delta += credit_density;
|
||||
|
||||
Parent::remove_(least_valuable_it);
|
||||
}
|
||||
|
||||
private:
|
||||
typedef Landlord_Naive<Key, Entry> Parent;
|
||||
|
||||
// sort iterators by credit_density of the Entry they reference.
|
||||
struct CD_greater
|
||||
{
|
||||
bool operator()(MapIt it1, MapIt it2) const
|
||||
{
|
||||
return Map::entry_from_it(it1).credit_density() >
|
||||
Map::entry_from_it(it2).credit_density();
|
||||
}
|
||||
};
|
||||
// wrapper on top of priority_queue that allows 'heap re-sift'
|
||||
// (see on_access).
|
||||
// notes:
|
||||
// - greater comparator makes pri_q.top() the one with
|
||||
// LEAST credit_density, which is what we want.
|
||||
// - deriving from an STL container is a bit dirty, but we need this
|
||||
// to get at the underlying data (priority_queue interface is not
|
||||
// very capable).
|
||||
class PriQ: public std::priority_queue<MapIt, std::vector<MapIt>, CD_greater>
|
||||
{
|
||||
public:
|
||||
void ensure_heap_order()
|
||||
{
|
||||
// TODO: this is actually N*logN - ouch! that explains high
|
||||
// CPU cost in profile. this is called after only 1 item has
|
||||
// changed, so a logN "sift" operation ought to suffice.
|
||||
// that's not supported by the STL heap functions, so we'd
|
||||
// need a better implementation. pending..
|
||||
std::make_heap(this->c.begin(), this->c.end(), this->comp);
|
||||
}
|
||||
};
|
||||
PriQ pri_q;
|
||||
|
||||
// delta values that have accumulated over several
|
||||
// remove_least_valuable() calls. applied during add().
|
||||
float pending_delta;
|
||||
|
||||
void commit_pending_delta()
|
||||
{
|
||||
if(pending_delta > 0.0f)
|
||||
{
|
||||
this->charge_all(pending_delta);
|
||||
pending_delta = 0.0f;
|
||||
|
||||
// we've changed entry credit, so the heap order *may* have been
|
||||
// violated; reorder the pri queue. (I don't think so,
|
||||
// due to definition of delta, but we'll play it safe)
|
||||
pri_q.ensure_heap_order();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
//
|
||||
// functor that implements division of first arg by second
|
||||
//
|
||||
|
||||
// this is used to calculate credit_density(); performance matters
|
||||
// because this is called for each entry during each remove operation.
|
||||
|
||||
// floating-point division (fairly slow)
|
||||
class Divider_Naive
|
||||
{
|
||||
public:
|
||||
Divider_Naive() {} // needed for default CacheEntry ctor
|
||||
Divider_Naive(float /*x*/) {}
|
||||
float operator()(float val, float divisor) const
|
||||
{
|
||||
return val / divisor;
|
||||
}
|
||||
};
|
||||
|
||||
// caches reciprocal of divisor and multiplies by that.
|
||||
// tradeoff: only 4 clocks (instead of 20), but 4 bytes extra per entry.
|
||||
class Divider_Recip
|
||||
{
|
||||
float recip;
|
||||
public:
|
||||
Divider_Recip() {} // needed for default CacheEntry ctor
|
||||
Divider_Recip(float x) { recip = 1.0f / x; }
|
||||
float operator()(float val, float /*divisor*/) const
|
||||
{
|
||||
return val * recip;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// initial implementation for testing purposes; quite inefficient.
|
||||
template<typename Key, typename Entry>
|
||||
class LRU
|
||||
{
|
||||
public:
|
||||
bool empty() const
|
||||
{
|
||||
return lru.empty();
|
||||
}
|
||||
|
||||
void add(const Key& key, const Entry& entry)
|
||||
{
|
||||
lru.push_back(KeyAndEntry(key, entry));
|
||||
}
|
||||
|
||||
bool find(const Key& key, const Entry** pentry) const
|
||||
{
|
||||
CIt it = std::find(lru.begin(), lru.end(), KeyAndEntry(key));
|
||||
if(it == lru.end())
|
||||
return false;
|
||||
*pentry = &it->entry;
|
||||
return true;
|
||||
}
|
||||
|
||||
void remove(const Key& key)
|
||||
{
|
||||
lru.remove(KeyAndEntry(key));
|
||||
}
|
||||
|
||||
void on_access(Entry& entry)
|
||||
{
|
||||
for(It it = lru.begin(); it != lru.end(); ++it)
|
||||
{
|
||||
if(&entry == &it->entry)
|
||||
{
|
||||
add(it->key, it->entry);
|
||||
lru.erase(it);
|
||||
return;
|
||||
}
|
||||
}
|
||||
DEBUG_WARN_ERR(ERR::LOGIC); // entry not found in list
|
||||
}
|
||||
|
||||
void remove_least_valuable(std::list<Entry>& entry_list)
|
||||
{
|
||||
entry_list.push_back(lru.front().entry);
|
||||
lru.pop_front();
|
||||
}
|
||||
|
||||
private:
|
||||
struct KeyAndEntry
|
||||
{
|
||||
KeyAndEntry(const Key& key): key(key) {}
|
||||
KeyAndEntry(const Key& key, const Entry& entry): key(key), entry(entry) {}
|
||||
|
||||
bool operator==(const KeyAndEntry& rhs) const { return key == rhs.key; }
|
||||
bool operator!=(const KeyAndEntry& rhs) const { return !operator==(rhs); }
|
||||
|
||||
Key key;
|
||||
Entry entry;
|
||||
};
|
||||
|
||||
typedef std::list<KeyAndEntry> List;
|
||||
typedef typename List::iterator It;
|
||||
typedef typename List::const_iterator CIt;
|
||||
List lru;
|
||||
};
|
||||
|
||||
|
||||
// this is applicable to all cache management policies and stores all
|
||||
// required information. a Divider functor is used to implement
|
||||
// division for credit_density.
|
||||
template<class Item, class Divider> struct CacheEntry
|
||||
{
|
||||
Item item;
|
||||
size_t size;
|
||||
size_t cost;
|
||||
float credit;
|
||||
|
||||
Divider divider;
|
||||
|
||||
// needed for mgr.remove_least_valuable's entry_copy
|
||||
CacheEntry()
|
||||
{
|
||||
}
|
||||
|
||||
CacheEntry(const Item& item_, size_t size_, size_t cost_)
|
||||
: item(item_), divider((float)size_)
|
||||
{
|
||||
size = size_;
|
||||
cost = cost_;
|
||||
credit = (float)cost;
|
||||
|
||||
// else divider will fail
|
||||
ENSURE(size != 0);
|
||||
}
|
||||
|
||||
float credit_density() const
|
||||
{
|
||||
return divider(credit, (float)size);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
//
|
||||
// Cache
|
||||
//
|
||||
|
||||
template
|
||||
<
|
||||
typename Key, typename Item,
|
||||
// see documentation above for Manager's interface.
|
||||
template<typename Key_, class Entry> class Manager = Landlord_Cached,
|
||||
class Divider = Divider_Naive
|
||||
>
|
||||
class Cache
|
||||
{
|
||||
public:
|
||||
Cache() : mgr() {}
|
||||
|
||||
void add(const Key& key, const Item& item, size_t size, size_t cost)
|
||||
{
|
||||
return mgr.add(key, Entry(item, size, cost));
|
||||
}
|
||||
|
||||
// remove the entry identified by <key>. expected usage is to check
|
||||
// if present and determine size via retrieve(), so no need for
|
||||
// error checking.
|
||||
// useful for invalidating single cache entries.
|
||||
void remove(const Key& key)
|
||||
{
|
||||
mgr.remove(key);
|
||||
}
|
||||
|
||||
// if there is no entry for <key> in the cache, return false.
|
||||
// otherwise, return true and pass back item and (optionally) size.
|
||||
//
|
||||
// if refill_credit (default), the cache manager 'rewards' this entry,
|
||||
// tending to keep it in cache longer. this parameter is not used in
|
||||
// normal operation - it's only for special cases where we need to
|
||||
// make an end run around the cache accounting (e.g. for cache simulator).
|
||||
bool retrieve(const Key& key, Item& item, size_t* psize = 0, bool refill_credit = true)
|
||||
{
|
||||
const Entry* entry;
|
||||
if(!mgr.find(key, &entry))
|
||||
return false;
|
||||
|
||||
item = entry->item;
|
||||
if(psize)
|
||||
*psize = entry->size;
|
||||
|
||||
if(refill_credit)
|
||||
mgr.on_access((Entry&)*entry);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool peek(const Key& key, Item& item, size_t* psize = 0)
|
||||
{
|
||||
return retrieve(key, item, psize, false);
|
||||
}
|
||||
|
||||
// toss out the least valuable entry. return false if cache is empty,
|
||||
// otherwise true and (optionally) pass back its item and size.
|
||||
bool remove_least_valuable(Item* pItem = 0, size_t* pSize = 0)
|
||||
{
|
||||
// as an artefact of the cache eviction policy, several entries
|
||||
// may be "shaken loose" by one call to remove_least_valuable.
|
||||
// we cache them in a list to disburden callers (they always get
|
||||
// exactly one).
|
||||
if(entries_awaiting_eviction.empty())
|
||||
{
|
||||
if(empty())
|
||||
return false;
|
||||
|
||||
mgr.remove_least_valuable(entries_awaiting_eviction);
|
||||
ENSURE(!entries_awaiting_eviction.empty());
|
||||
}
|
||||
|
||||
const Entry& entry = entries_awaiting_eviction.front();
|
||||
if(pItem)
|
||||
*pItem = entry.item;
|
||||
if(pSize)
|
||||
*pSize = entry.size;
|
||||
entries_awaiting_eviction.pop_front();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool empty() const
|
||||
{
|
||||
return mgr.empty();
|
||||
}
|
||||
|
||||
private:
|
||||
typedef CacheEntry<Item, Divider> Entry;
|
||||
|
||||
// see note in remove_least_valuable().
|
||||
std::list<Entry> entries_awaiting_eviction;
|
||||
|
||||
Manager<Key, Entry> mgr;
|
||||
};
|
||||
|
||||
#endif // #ifndef INCLUDED_CACHE_ADT
|
||||
@@ -1,140 +0,0 @@
|
||||
/* Copyright (C) 2025 Wildfire Games.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining
|
||||
* a copy of this software and associated documentation files (the
|
||||
* "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||
* permit persons to whom the Software is furnished to do so, subject to
|
||||
* the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included
|
||||
* in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#include "lib/self_test.h"
|
||||
|
||||
#include "lib/adts/cache_adt.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <queue>
|
||||
#include <random>
|
||||
|
||||
class TestCache: public CxxTest::TestSuite
|
||||
{
|
||||
public:
|
||||
void test_cache_perf()
|
||||
{
|
||||
Cache<int, int, Landlord_Naive> c1;
|
||||
Cache<int, int, Landlord_Naive, Divider_Recip> c1r;
|
||||
Cache<int, int, Landlord_Cached> c2;
|
||||
Cache<int, int, Landlord_Cached, Divider_Recip> c2r;
|
||||
Cache<int, int, Landlord_Lazy> c3;
|
||||
Cache<int, int, Landlord_Lazy, Divider_Recip> c3r;
|
||||
|
||||
#if defined(ENABLE_CACHE_POLICY_BENCHMARK) || 0
|
||||
// set max priority, to reduce interference while measuring.
|
||||
int old_policy; static sched_param old_param; // (static => 0-init)
|
||||
pthread_getschedparam(pthread_self(), &old_policy, &old_param);
|
||||
static sched_param max_param;
|
||||
max_param.sched_priority = sched_get_priority_max(SCHED_FIFO);
|
||||
pthread_setschedparam(pthread_self(), SCHED_FIFO, &max_param);
|
||||
|
||||
#define MEASURE(c, desc)\
|
||||
{\
|
||||
std::mt19937 engine(42);\
|
||||
std::uniform_int_distribution<int> distribution9(0, 9);\
|
||||
std::uniform_int_distribution<size_t> distribution99(1, 99);\
|
||||
int cnt = 1;\
|
||||
const double startTime = timer_Time();\
|
||||
for(int i = 0; i < 30000; i++)\
|
||||
{\
|
||||
/* 70% add (random objects) */\
|
||||
const bool add = distribution9(engine) < 7;\
|
||||
if(add)\
|
||||
{\
|
||||
int key = cnt++;\
|
||||
int val = cnt++;\
|
||||
size_t size = distribution(engine);\
|
||||
size_t cost = distribution(engine);\
|
||||
c.add(key, val, size, cost);\
|
||||
}\
|
||||
else\
|
||||
{\
|
||||
size_t size;\
|
||||
int value;\
|
||||
c.remove_least_valuable(&value, &size);\
|
||||
}\
|
||||
}\
|
||||
debug_printf("%s took %f s", desc, timer_Time() - startTime);\
|
||||
}
|
||||
MEASURE(c1, "naive")
|
||||
MEASURE(c1r, "naiverecip")
|
||||
MEASURE(c2, "cached")
|
||||
MEASURE(c2r, "cachedrecip")
|
||||
MEASURE(c3, "lazy")
|
||||
MEASURE(c3r, "lazyrecip")
|
||||
|
||||
// restore previous policy and priority.
|
||||
pthread_setschedparam(pthread_self(), old_policy, &old_param);
|
||||
exit(1134);
|
||||
#endif
|
||||
}
|
||||
|
||||
// ensures all 3 variants of Landlord<> behave the same
|
||||
// [PT: disabled because it's far too slow]
|
||||
void DISABLED_test_cache_policies()
|
||||
{
|
||||
std::mt19937 engine(42);
|
||||
std::uniform_int_distribution<int> distribution9(0, 9);
|
||||
std::uniform_int_distribution<size_t> distribution99(1, 99);
|
||||
|
||||
Cache<int, int, Landlord_Naive> c1;
|
||||
Cache<int, int, Landlord_Cached> c2;
|
||||
Cache<int, int, Landlord_Lazy> c3;
|
||||
|
||||
int cnt = 1;
|
||||
for(int i = 0; i < 1000; i++)
|
||||
{
|
||||
// 70% add (random objects)
|
||||
const bool add = distribution9(engine) < 7;
|
||||
if(add)
|
||||
{
|
||||
int key = cnt++;
|
||||
int val = cnt++;
|
||||
size_t size = distribution99(engine);
|
||||
size_t cost = distribution99(engine);
|
||||
c1.add(key, val, size, cost);
|
||||
c2.add(key, val, size, cost);
|
||||
c3.add(key, val, size, cost);
|
||||
}
|
||||
// 30% delete - make sure "least valuable" was same for all
|
||||
else
|
||||
{
|
||||
size_t size1, size2, size3;
|
||||
int value1, value2, value3;
|
||||
bool removed1, removed2, removed3;
|
||||
removed1 = c1.remove_least_valuable(&value1, &size1);
|
||||
removed2 = c2.remove_least_valuable(&value2, &size2);
|
||||
removed3 = c3.remove_least_valuable(&value3, &size3);
|
||||
TS_ASSERT_EQUALS(removed1, removed2);
|
||||
TS_ASSERT_EQUALS(removed2, removed3);
|
||||
if (removed1)
|
||||
{
|
||||
TS_ASSERT_EQUALS(size1, size2);
|
||||
TS_ASSERT_EQUALS(value1, value2);
|
||||
TS_ASSERT_EQUALS(size2, size3);
|
||||
TS_ASSERT_EQUALS(value2, value3);
|
||||
}
|
||||
} // else
|
||||
} // for i
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user