Skip to content

Commit 807df4f

Browse files
Copilot1a1a11a
andcommitted
Implement LRU-K cache eviction algorithm
- Add full LRU-K implementation in LRU_K.cpp - Objects with < K accesses are evicted in FIFO order (infinite backward K-distance) - Objects with >= K accesses are sorted by K-th most recent access time - Default K=2, configurable via "k=N" parameter - Add LRU_K_init declaration to evictionAlgo.h - Register "lru-k" in cache_init.h simple_algos array - Add LRU_K.cpp to CMakeLists.txt build - Update test/common.h to enable LRU_K_init, remove dead code - Add test data and test function in test_evictionAlgo.c - All 28 tests pass including new cacheAlgo_LRU_K test Co-authored-by: 1a1a11a <[email protected]> Agent-Logs-Url: https://github.com/1a1a11a/libCacheSim/sessions/ba5ad55c-e5e5-4bc1-a3d0-a355843709d2
1 parent 84c67cd commit 807df4f

6 files changed

Lines changed: 378 additions & 13 deletions

File tree

libCacheSim/bin/cachesim/cache_init.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ static inline cache_t *create_cache(const char *trace_path,
5656
{"lfuda", LFUDA_init},
5757
{"lirs", LIRS_init},
5858
{"lru", LRU_init},
59+
{"lru-k", LRU_K_init},
5960
{"lru-prob", LRU_Prob_init},
6061
{"nop", nop_init},
6162
// plugin cache that allows user to implement custom cache

libCacheSim/cache/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ set(eviction_sources_c
7575
set(eviction_sources_cpp
7676
eviction/cpp/LFU.cpp
7777
eviction/cpp/GDSF.cpp
78+
eviction/cpp/LRU_K.cpp
7879
eviction/LHD/lhd.cpp
7980
eviction/LHD/LHD_Interface.cpp
8081
)
Lines changed: 350 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,350 @@
1+
/* LRU-K: Evict the object with the largest backward K-distance.
2+
*
3+
* Objects that have been accessed fewer than K times have an infinite
4+
* backward K-distance and are evicted first in FIFO order.
5+
* Among objects with >= K accesses, the one with the oldest K-th most
6+
* recent access time is evicted first.
7+
*
8+
* Reference: O'Neil, O'Neil, Weikum. "The LRU-K Page Replacement Algorithm
9+
* for Database Disk Buffering." ACM SIGMOD 1993.
10+
*
11+
* Parameters:
12+
* k: the number of recent accesses to track (default: 2)
13+
*/
14+
15+
#include <cassert>
16+
#include <deque>
17+
#include <list>
18+
#include <set>
19+
#include <string>
20+
#include <unordered_map>
21+
#include <utility>
22+
23+
#include "abstractRank.hpp"
24+
25+
namespace eviction {
26+
27+
class LRU_K {
28+
public:
29+
int k;
30+
31+
/* Objects with < K accesses are held in a FIFO queue.
32+
* We use a doubly-linked list + map for O(1) removal. */
33+
std::list<cache_obj_t *> fifo_queue;
34+
std::unordered_map<cache_obj_t *, std::list<cache_obj_t *>::iterator>
35+
fifo_map;
36+
37+
/* Objects with >= K accesses are held in a priority queue sorted by
38+
* their K-th most recent access time (ascending = evict first). */
39+
using pq_entry_t = std::pair<int64_t, obj_id_t>;
40+
std::set<pq_entry_t> pq;
41+
std::unordered_map<cache_obj_t *, int64_t> pq_map; // obj -> K-th vtime
42+
43+
/* Per-object access history: the K most recent request vtimes.
44+
* front() = oldest (K-th most recent), back() = most recent. */
45+
std::unordered_map<cache_obj_t *, std::deque<int64_t>> history;
46+
47+
explicit LRU_K(int k_param = 2) : k(k_param) {}
48+
};
49+
50+
} // namespace eviction
51+
52+
#ifdef __cplusplus
53+
extern "C" {
54+
#endif
55+
56+
// ***********************************************************************
57+
// **** ****
58+
// **** function declarations ****
59+
// **** ****
60+
// ***********************************************************************
61+
62+
cache_t *LRU_K_init(const common_cache_params_t ccache_params,
63+
const char *cache_specific_params);
64+
static void LRU_K_free(cache_t *cache);
65+
static bool LRU_K_get(cache_t *cache, const request_t *req);
66+
67+
static cache_obj_t *LRU_K_find(cache_t *cache, const request_t *req,
68+
bool update_cache);
69+
static cache_obj_t *LRU_K_insert(cache_t *cache, const request_t *req);
70+
static cache_obj_t *LRU_K_to_evict(cache_t *cache, const request_t *req);
71+
static void LRU_K_evict(cache_t *cache, const request_t *req);
72+
static bool LRU_K_remove(cache_t *cache, obj_id_t obj_id);
73+
74+
// ***********************************************************************
75+
// **** ****
76+
// **** end user facing functions ****
77+
// **** ****
78+
// **** init, free, get ****
79+
// ***********************************************************************
80+
81+
/**
82+
* @brief parse algorithm-specific parameters
83+
*
84+
* Supported parameters:
85+
* k=<int> number of accesses to track (default: 2)
86+
*/
87+
static void LRU_K_parse_params(cache_t *cache,
88+
const char *cache_specific_params) {
89+
auto *lruk =
90+
reinterpret_cast<eviction::LRU_K *>(cache->eviction_params);
91+
if (cache_specific_params == nullptr || cache_specific_params[0] == '\0')
92+
return;
93+
94+
const char *k_str = strstr(cache_specific_params, "k=");
95+
if (k_str != nullptr) {
96+
int k_val = atoi(k_str + 2);
97+
if (k_val >= 1) {
98+
lruk->k = k_val;
99+
} else {
100+
WARN("LRU_K: invalid k value %d, using default k=2\n", k_val);
101+
}
102+
}
103+
}
104+
105+
/**
106+
* @brief initialize the cache
107+
*
108+
* @param ccache_params some common cache parameters
109+
* @param cache_specific_params cache specific parameters, e.g. "k=2"
110+
*/
111+
cache_t *LRU_K_init(const common_cache_params_t ccache_params,
112+
const char *cache_specific_params) {
113+
cache_t *cache =
114+
cache_struct_init("LRU_K", ccache_params, cache_specific_params);
115+
cache->eviction_params = reinterpret_cast<void *>(new eviction::LRU_K(2));
116+
117+
cache->cache_init = LRU_K_init;
118+
cache->cache_free = LRU_K_free;
119+
cache->get = LRU_K_get;
120+
cache->find = LRU_K_find;
121+
cache->insert = LRU_K_insert;
122+
cache->evict = LRU_K_evict;
123+
cache->to_evict = LRU_K_to_evict;
124+
cache->remove = LRU_K_remove;
125+
126+
if (ccache_params.consider_obj_metadata) {
127+
cache->obj_md_size = 8;
128+
} else {
129+
cache->obj_md_size = 0;
130+
}
131+
132+
LRU_K_parse_params(cache, cache_specific_params);
133+
134+
return cache;
135+
}
136+
137+
/**
138+
* free resources used by this cache
139+
*
140+
* @param cache
141+
*/
142+
static void LRU_K_free(cache_t *cache) {
143+
delete reinterpret_cast<eviction::LRU_K *>(cache->eviction_params);
144+
cache_struct_free(cache);
145+
}
146+
147+
/**
148+
* @brief this function is the user facing API
149+
* it performs the following logic
150+
*
151+
* ```
152+
* if obj in cache:
153+
* update_metadata
154+
* return true
155+
* else:
156+
* if cache does not have enough space:
157+
* evict until it has space to insert
158+
* insert the object
159+
* return false
160+
* ```
161+
*
162+
* @param cache
163+
* @param req
164+
* @return true if cache hit, false if cache miss
165+
*/
166+
static bool LRU_K_get(cache_t *cache, const request_t *req) {
167+
return cache_get_base(cache, req);
168+
}
169+
170+
// ***********************************************************************
171+
// **** ****
172+
// **** developer facing APIs (used by cache developer) ****
173+
// **** ****
174+
// ***********************************************************************
175+
176+
/**
177+
* @brief find an object in the cache
178+
*
179+
* @param cache
180+
* @param req
181+
* @param update_cache whether to update the cache metadata
182+
* @return the object or NULL if not found
183+
*/
184+
static cache_obj_t *LRU_K_find(cache_t *cache, const request_t *req,
185+
bool update_cache) {
186+
auto *lruk =
187+
reinterpret_cast<eviction::LRU_K *>(cache->eviction_params);
188+
cache_obj_t *obj = cache_find_base(cache, req, update_cache);
189+
190+
if (obj != nullptr && update_cache) {
191+
int64_t vtime = cache->n_req;
192+
auto &hist = lruk->history[obj];
193+
194+
/* Add the current access to history and maintain window of size K */
195+
hist.push_back(vtime);
196+
if ((int)hist.size() > lruk->k) {
197+
hist.pop_front();
198+
}
199+
200+
bool in_fifo = (lruk->fifo_map.count(obj) > 0);
201+
202+
if (in_fifo && (int)hist.size() >= lruk->k) {
203+
/* Object has accumulated K accesses: graduate from FIFO to PQ */
204+
lruk->fifo_queue.erase(lruk->fifo_map[obj]);
205+
lruk->fifo_map.erase(obj);
206+
207+
int64_t kth_vtime = hist.front();
208+
obj_id_t oid = obj->obj_id;
209+
lruk->pq.insert({kth_vtime, oid});
210+
lruk->pq_map[obj] = kth_vtime;
211+
} else if (!in_fifo) {
212+
/* Object is already in PQ: update its K-th vtime priority */
213+
int64_t old_kth = lruk->pq_map[obj];
214+
obj_id_t oid = obj->obj_id;
215+
lruk->pq.erase({old_kth, oid});
216+
int64_t new_kth = hist.front();
217+
lruk->pq.insert({new_kth, oid});
218+
lruk->pq_map[obj] = new_kth;
219+
}
220+
/* else: still in FIFO with < K accesses, no PQ update needed */
221+
}
222+
223+
return obj;
224+
}
225+
226+
/**
227+
* @brief insert an object into the cache.
228+
* Assumes the cache has enough space; eviction should be performed before
229+
* calling this function.
230+
*
231+
* @param cache
232+
* @param req
233+
* @return the inserted object
234+
*/
235+
static cache_obj_t *LRU_K_insert(cache_t *cache, const request_t *req) {
236+
auto *lruk =
237+
reinterpret_cast<eviction::LRU_K *>(cache->eviction_params);
238+
239+
cache_obj_t *obj = cache_insert_base(cache, req);
240+
241+
int64_t vtime = cache->n_req;
242+
lruk->history[obj] = {vtime};
243+
244+
if (lruk->k == 1) {
245+
/* K=1: every object immediately enters the PQ (equivalent to LRU) */
246+
obj_id_t oid = obj->obj_id;
247+
lruk->pq.insert({vtime, oid});
248+
lruk->pq_map[obj] = vtime;
249+
} else {
250+
/* Objects with < K accesses go to the FIFO queue */
251+
lruk->fifo_queue.push_back(obj);
252+
lruk->fifo_map[obj] = std::prev(lruk->fifo_queue.end());
253+
}
254+
255+
return obj;
256+
}
257+
258+
/**
259+
* @brief find the object to be evicted without actually evicting it
260+
*
261+
* @param cache the cache
262+
* @return the object to be evicted
263+
*/
264+
static cache_obj_t *LRU_K_to_evict(cache_t *cache, const request_t *req) {
265+
auto *lruk =
266+
reinterpret_cast<eviction::LRU_K *>(cache->eviction_params);
267+
268+
if (!lruk->fifo_queue.empty()) {
269+
return lruk->fifo_queue.front();
270+
}
271+
if (!lruk->pq.empty()) {
272+
/* pq is ordered by (kth_vtime, obj_id) ascending: the front has the
273+
* smallest K-th access vtime, which corresponds to the oldest K-th
274+
* access and therefore the largest backward K-distance */
275+
obj_id_t evict_id = lruk->pq.begin()->second;
276+
return hashtable_find_obj_id(cache->hashtable, evict_id);
277+
}
278+
return nullptr;
279+
}
280+
281+
/**
282+
* @brief evict an object from the cache
283+
*
284+
* @param cache
285+
* @param req not used
286+
*/
287+
static void LRU_K_evict(cache_t *cache, const request_t *req) {
288+
auto *lruk =
289+
reinterpret_cast<eviction::LRU_K *>(cache->eviction_params);
290+
291+
cache_obj_t *obj;
292+
293+
if (!lruk->fifo_queue.empty()) {
294+
/* Evict from FIFO queue first (infinite backward K-distance) */
295+
obj = lruk->fifo_queue.front();
296+
lruk->fifo_queue.pop_front();
297+
lruk->fifo_map.erase(obj);
298+
} else {
299+
/* Evict from PQ: smallest K-th vtime = largest backward K-distance */
300+
DEBUG_ASSERT(!lruk->pq.empty());
301+
auto it = lruk->pq.begin();
302+
obj_id_t evict_id = it->second;
303+
lruk->pq.erase(it);
304+
obj = hashtable_find_obj_id(cache->hashtable, evict_id);
305+
DEBUG_ASSERT(obj != nullptr);
306+
lruk->pq_map.erase(obj);
307+
}
308+
309+
lruk->history.erase(obj);
310+
cache_remove_obj_base(cache, obj, true);
311+
}
312+
313+
/**
314+
* @brief remove an object from the cache by user request
315+
*
316+
* @param cache
317+
* @param obj_id
318+
* @return true if removed, false if not found
319+
*/
320+
static bool LRU_K_remove(cache_t *cache, obj_id_t obj_id) {
321+
auto *lruk =
322+
reinterpret_cast<eviction::LRU_K *>(cache->eviction_params);
323+
324+
cache_obj_t *obj = hashtable_find_obj_id(cache->hashtable, obj_id);
325+
if (obj == nullptr) {
326+
return false;
327+
}
328+
329+
lruk->history.erase(obj);
330+
331+
auto fifo_it = lruk->fifo_map.find(obj);
332+
if (fifo_it != lruk->fifo_map.end()) {
333+
lruk->fifo_queue.erase(fifo_it->second);
334+
lruk->fifo_map.erase(fifo_it);
335+
} else {
336+
auto pq_it = lruk->pq_map.find(obj);
337+
if (pq_it != lruk->pq_map.end()) {
338+
int64_t kth_vtime = pq_it->second;
339+
lruk->pq.erase({kth_vtime, obj_id});
340+
lruk->pq_map.erase(pq_it);
341+
}
342+
}
343+
344+
cache_remove_obj_base(cache, obj, true);
345+
return true;
346+
}
347+
348+
#ifdef __cplusplus
349+
}
350+
#endif

libCacheSim/include/libCacheSim/evictionAlgo.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,9 @@ cache_t *LRU_Prob_init(const common_cache_params_t ccache_params,
110110
cache_t *LRU_init(const common_cache_params_t ccache_params,
111111
const char *cache_specific_params);
112112

113+
cache_t *LRU_K_init(const common_cache_params_t ccache_params,
114+
const char *cache_specific_params);
115+
113116
cache_t *LRUv0_init(const common_cache_params_t ccache_params,
114117
const char *cache_specific_params);
115118

test/common.h

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -227,10 +227,8 @@ static cache_t *create_test_cache(const char *alg_name,
227227
cache = Random_init(cc_params, NULL);
228228
} else if (strcasecmp(alg_name, "MRU") == 0) {
229229
cache = MRU_init(cc_params, NULL);
230-
// } else if (strcmp(alg_name, "LRU_K") == 0) {
231-
// cache = LRU_K_init(cc_params, NULL);
232-
} else if (strcasecmp(alg_name, "LFU") == 0) {
233-
cache = LFU_init(cc_params, NULL);
230+
} else if (strcasecmp(alg_name, "LRU_K") == 0) {
231+
cache = LRU_K_init(cc_params, NULL);
234232
} else if (strcasecmp(alg_name, "LFU") == 0) {
235233
cache = LFU_init(cc_params, NULL);
236234
} else if (strcasecmp(alg_name, "LFUDA") == 0) {

0 commit comments

Comments
 (0)