Skip to content

Commit 6239975

Browse files
Copilot1a1a11a
andauthored
Implement LRU-K eviction algorithm (#311)
* Initial plan * 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 <8584783+1a1a11a@users.noreply.github.com> Agent-Logs-Url: https://github.com/1a1a11a/libCacheSim/sessions/ba5ad55c-e5e5-4bc1-a3d0-a355843709d2 * LRU_K: harden param parsing and account for K in obj_md_size - Replace `strstr("k=") + atoi` with `strsep`-based key=value tokenizer that matches the SLRU pattern. Avoids fragile substring matches (e.g. embedded "k=" in unrelated keys), validates the value with `strtol` (no silent truncation), errors on unknown keys, and adds a `print` parameter consistent with other algos. - Reorder LRU_K_init so parse_params runs before obj_md_size, and scale per-object metadata as `8 * k + 16` (history vtimes + node overhead) instead of a fixed 8 bytes that ignored K. * LRU_K: address review comments - Add <iterator> include for std::prev (Copilot review). - Use cache_evict_base in LRU_K_evict, matching the convention in LRU.c/FIFO.c so eviction-age tracking and the prefetcher hook fire on capacity-driven evictions. cache_remove_obj_base is kept for user-initiated removal in LRU_K_remove. (haochengxia review) * LRU_K: apply clang-format --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: 1a1a11a <8584783+1a1a11a@users.noreply.github.com> Co-authored-by: Juncheng Yang <peter.waynechina@gmail.com> Co-authored-by: Juncheng Yang <1a1a11a@users.noreply.github.com>
1 parent 2eb8a9e commit 6239975

6 files changed

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

0 commit comments

Comments
 (0)