diff --git a/libCacheSim/cache/eviction/SLRU.c b/libCacheSim/cache/eviction/SLRU.c index 875f09e4..6dbd2c42 100644 --- a/libCacheSim/cache/eviction/SLRU.c +++ b/libCacheSim/cache/eviction/SLRU.c @@ -26,6 +26,7 @@ typedef struct SLRU_params { int64_t *lru_n_objs; int64_t *lru_max_n_bytes; int n_seg; + double seg_ratio[SLRU_MAX_N_SEG]; } SLRU_params_t; // *********************************************************************** @@ -140,6 +141,7 @@ cache_t *SLRU_init(const common_cache_params_t ccache_params, params->lru_max_n_bytes[i] = (int64_t)(ccache_params.cache_size / params->n_seg) + (i >= params->n_seg - remainder); + params->seg_ratio[i] = 1.0 / params->n_seg; } } @@ -261,9 +263,12 @@ static cache_obj_t *SLRU_find(cache_t *cache, const request_t *req, } #endif - if (obj->SLRU.lru_id == params->n_seg - 1) { - move_obj_to_head(¶ms->lru_heads[params->n_seg - 1], - ¶ms->lru_tails[params->n_seg - 1], obj); + int id = obj->SLRU.lru_id; + int64_t sz = obj->obj_size + cache->obj_md_size; + if (id == params->n_seg - 1 || (sz > params->lru_max_n_bytes[id] && + sz > params->lru_max_n_bytes[id + 1])) { + // an object that fits neither segment (after SLRU_resize) stays here + move_obj_to_head(¶ms->lru_heads[id], ¶ms->lru_tails[id], obj); } else { SLRU_promote_to_next_seg(cache, req, obj); @@ -489,9 +494,9 @@ static void SLRU_parse_params(cache_t *cache, params->n_seg = n_seg; params->lru_max_n_bytes = calloc(params->n_seg, sizeof(int64_t)); for (int i = 0; i < n_seg; i++) { + params->seg_ratio[i] = (double)seg_size_array[i] / seg_size_sum; params->lru_max_n_bytes[i] = - (int64_t)((double)seg_size_array[i] / seg_size_sum * - cache->cache_size); + (int64_t)(params->seg_ratio[i] * cache->cache_size); if (params->lru_max_n_bytes[i] <= 0) { ERROR("Invalid segment size for segment %d: %lld bytes\n", i, (long long)params->lru_max_n_bytes[i]); @@ -514,6 +519,18 @@ static void SLRU_parse_params(cache_t *cache, // **** internal functions **** // **** **** // *********************************************************************** +/* change the cache size and keep the share of each segment, no eviction */ +void SLRU_resize(cache_t *cache, int64_t new_size) { + SLRU_params_t *params = (SLRU_params_t *)(cache->eviction_params); + int64_t n_assigned = 0; + for (int i = 0; i < params->n_seg - 1; i++) { + params->lru_max_n_bytes[i] = (int64_t)(params->seg_ratio[i] * new_size); + n_assigned += params->lru_max_n_bytes[i]; + } + params->lru_max_n_bytes[params->n_seg - 1] = new_size - n_assigned; + cache->cache_size = new_size; +} + /* SLRU cannot insert an object larger than segment size */ bool SLRU_can_insert(cache_t *cache, const request_t *req) { SLRU_params_t *params = (SLRU_params_t *)cache->eviction_params; diff --git a/libCacheSim/cache/eviction/WTinyLFU.c b/libCacheSim/cache/eviction/WTinyLFU.c index 0c91b6d4..2ad583c3 100644 --- a/libCacheSim/cache/eviction/WTinyLFU.c +++ b/libCacheSim/cache/eviction/WTinyLFU.c @@ -4,6 +4,15 @@ // combined with a windowed LRU structure (W-TinyLFU) // to resist burst traffic of temporal hot objects // +// hill-climb=1 adapts the window size with the hill climber in Adaptive +// Software Cache Management (Middleware'18): every 10 x cache size bytes of +// requests, move the window by 5% (1% to 80%), and turn around if the hit +// ratio dropped. Different from the paper, the window does not go to 0% +// (every request would miss), a tie does not turn around (as in Caffeine), +// and the period is in bytes (the same for unit-size objects). When the +// window grows, the main cache evicts down to its new size instead of moving +// objects into the window as Caffeine does +// // NOTE: it is NOT thread-safe // // part of the details borrowed from https://github.com/mandreyel/w-tinylfu @@ -41,9 +50,23 @@ typedef struct WTinyLFU_params { char main_cache_type[32]; request_t *req_local; + + bool hill_climb; + int window_step; + int climb_direction; + int64_t n_climb_req; + int64_t n_climb_hit; + int64_t n_climb_byte; + double prev_hit_ratio; } WTinyLFU_params_t; -static const char *DEFAULT_PARAMS = "main-cache=SLRU,window-size=0.01"; +static const char *DEFAULT_PARAMS = + "main-cache=SLRU,window-size=0.01,hill-climb=0"; + +#define MAX_WINDOW_STEP 16 +static double WTinyLFU_window_ratio(int step) { + return step == 0 ? 0.01 : 0.05 * step; +} // *********************************************************************** // **** **** @@ -110,6 +133,17 @@ cache_t *WTinyLFU_init(const common_cache_params_t ccache_params, WTinyLFU_parse_params(cache, cache_specific_params); } + if (params->hill_climb) { + if (strcasecmp(params->main_cache_type, "LRU") != 0 && + strcasecmp(params->main_cache_type, "SLRU") != 0) { + ERROR("WTinyLFU hill-climb only supports LRU and SLRU main caches\n"); + } + params->window_step = + (int)MIN(lround(params->window_size / 0.05), MAX_WINDOW_STEP); + params->window_size = WTinyLFU_window_ratio(params->window_step); + params->climb_direction = 1; + } + common_cache_params_t ccache_params_local = ccache_params; ccache_params_local.cache_size *= params->window_size; @@ -163,8 +197,9 @@ cache_t *WTinyLFU_init(const common_cache_params_t ccache_params, cache->obj_md_size = params->LRU->obj_md_size; } - snprintf(cache->cache_name, CACHE_NAME_ARRAY_LEN, "WTinyLFU-w%.2lf-%s", - params->window_size, params->main_cache_type); + snprintf(cache->cache_name, CACHE_NAME_ARRAY_LEN, "WTinyLFU-w%.2lf-%s%s", + params->window_size, params->main_cache_type, + params->hill_climb ? "-climb" : ""); params->req_local = new_request(); params->n_admit_bytes = 0; @@ -216,12 +251,53 @@ static void WTinyLFU_free(cache_t *cache) { cache_struct_free(cache); } +static void WTinyLFU_resize_window(cache_t *cache, const request_t *req) { + WTinyLFU_params_t *params = (WTinyLFU_params_t *)(cache->eviction_params); + cache_t *main_cache = params->main_cache; + double window_ratio = WTinyLFU_window_ratio(params->window_step); + params->LRU->cache_size = (int64_t)(cache->cache_size * window_ratio); + int64_t main_size = cache->cache_size - params->LRU->cache_size; + if (strcasecmp(params->main_cache_type, "SLRU") == 0) { + SLRU_resize(main_cache, main_size); + } else { + main_cache->cache_size = main_size; + } + while (main_cache->get_occupied_byte(main_cache) > main_cache->cache_size) { + main_cache->evict(main_cache, req); + } +} + +static void WTinyLFU_climb(cache_t *cache, const request_t *req, bool hit) { + WTinyLFU_params_t *params = (WTinyLFU_params_t *)(cache->eviction_params); + params->n_climb_req += 1; + params->n_climb_hit += hit; + params->n_climb_byte += req->obj_size; + if (params->n_climb_byte < 10 * cache->cache_size) { + return; + } + + double hit_ratio = (double)params->n_climb_hit / params->n_climb_req; + if (hit_ratio < params->prev_hit_ratio) { + params->climb_direction = -params->climb_direction; + } + params->prev_hit_ratio = hit_ratio; + params->n_climb_req = params->n_climb_hit = params->n_climb_byte = 0; + + params->window_step += params->climb_direction; + params->window_step = MAX(0, MIN(MAX_WINDOW_STEP, params->window_step)); + WTinyLFU_resize_window(cache, req); +} + static bool WTinyLFU_get(cache_t *cache, const request_t *req) { /* because this field cannot be updated in time since segment LRUs are * updated, so we should not use this field */ DEBUG_ASSERT(cache->occupied_byte == 0); + WTinyLFU_params_t *params = (WTinyLFU_params_t *)(cache->eviction_params); bool ck = cache_get_base(cache, req); + if (params->hill_climb) { + WTinyLFU_climb(cache, req, ck); + } return ck; } @@ -383,10 +459,10 @@ static bool WTinyLFU_remove(cache_t *cache, obj_id_t obj_id) { * configured type, which is what `-e print` runs against */ static const char *WTinyLFU_current_params(WTinyLFU_params_t *params) { static __thread char params_str[128]; - snprintf(params_str, 128, "main-cache=%s,window-size=%.4lf", + snprintf(params_str, 128, "main-cache=%s,window-size=%.4lf,hill-climb=%d", params->main_cache == NULL ? params->main_cache_type : params->main_cache->cache_name, - params->window_size); + params->window_size, params->hill_climb); return params_str; } @@ -418,6 +494,12 @@ static void WTinyLFU_parse_params(cache_t *cache, error = "expected a nonempty value"; } else if (strcasecmp(key, "main-cache") == 0) { strncpy(params->main_cache_type, value, 30); + } else if (strcasecmp(key, "hill-climb") == 0) { + if (strcmp(value, "0") != 0 && strcmp(value, "1") != 0) { + error = "expected 0 or 1"; + } else { + params->hill_climb = value[0] == '1'; + } } else if (strcasecmp(key, "window-size") == 0) { char *end; errno = 0; diff --git a/libCacheSim/include/libCacheSim/evictionAlgo.h b/libCacheSim/include/libCacheSim/evictionAlgo.h index 811cbe20..80b9f520 100644 --- a/libCacheSim/include/libCacheSim/evictionAlgo.h +++ b/libCacheSim/include/libCacheSim/evictionAlgo.h @@ -224,6 +224,8 @@ cache_t *Size_init(const common_cache_params_t ccache_params, cache_t *SLRU_init(const common_cache_params_t ccache_params, const char *cache_specific_params); +void SLRU_resize(cache_t *cache, int64_t new_size); + cache_t *SLRUv0_init(const common_cache_params_t ccache_params, const char *cache_specific_params); diff --git a/test/test_cli.sh b/test/test_cli.sh index 93c59b03..2d084c94 100755 --- a/test/test_cli.sh +++ b/test/test_cli.sh @@ -307,7 +307,8 @@ for params in main-cache main-cache= main-cache,print \ window-size=abc,print window-size=0.1junk,print window-size=nan \ window-size=nan,print window-size=inf,print window-size=-inf,print \ window-size=-0.1,print window-size=1,print window-size=1e999,print \ - window-size=1e-999,print unknown=1; do + window-size=1e-999,print hill-climb hill-climb= hill-climb=2,print \ + hill-climb=yes,print unknown=1; do out=$("${BIN_DIR}/cachesim" "${TRACE}" vscsi wtinyLFU 1mb \ --num-req=10 --hashpower=8 -e "${params}" 2>&1) rc=$? @@ -320,7 +321,8 @@ for params in main-cache main-cache= main-cache,print \ fi done for params in '' window-size=0.01 window-size=1e-2 window-size=0.01, \ - 'main-cache=LRU,window-size=0.01' 'main-cache=FIFO, window-size=0.01'; do + 'main-cache=LRU,window-size=0.01' 'main-cache=FIFO, window-size=0.01' \ + hill-climb=0 hill-climb=1 'main-cache=LRU,hill-climb=1'; do expect_output "WTinyLFU accepts '${params}'" '10 req' \ "${BIN_DIR}/cachesim" "${TRACE}" vscsi tinyLFU 1mb \ --num-req=10 --hashpower=8 -e "${params}" @@ -333,6 +335,12 @@ for spec in 0:0.0000 0.01:0.0100 1e-2:0.0100 +0.01:0.0100 \ "${BIN_DIR}/cachesim" "${TRACE}" vscsi wtinyLFU 1mb \ --hashpower=8 -e "window-size=${value},print" done +expect_output "WTinyLFU reports hill-climb=1" "hill-climb=1" \ + "${BIN_DIR}/cachesim" "${TRACE}" vscsi wtinyLFU 1mb \ + --hashpower=8 -e "hill-climb=1,print" +expect_clean_error "WTinyLFU hill-climb rejects a FIFO main cache" \ + "${BIN_DIR}/cachesim" "${TRACE}" vscsi wtinyLFU 1mb \ + --num-req=10 --hashpower=8 -e "main-cache=FIFO,hill-climb=1" echo "running admission and prefetch missing-value tests" diff --git a/test/test_evictionAlgo.c b/test/test_evictionAlgo.c index db3500fe..36f87b56 100644 --- a/test/test_evictionAlgo.c +++ b/test/test_evictionAlgo.c @@ -623,6 +623,106 @@ static void test_WTinyLFU_counts_window_hits(void) { cache->cache_free(cache); } +// The first hill climbing decision, after 10 x cache size requests, grows the +// window from 1% to 5%, and the main cache evicts down to its new size. +static void test_WTinyLFU_hill_climb_grows_window(void) { + common_cache_params_t cc_params = {.cache_size = 100, .hashpower = 8}; + cache_t *cache = WTinyLFU_init(cc_params, "main-cache=LRU,hill-climb=1"); + request_t *req = new_request(); + req->obj_size = 1; + + // 101 moves 1 to 99 into the main cache + for (obj_id_t id = 1; id <= 101; id++) { + req->obj_id = id; + g_assert_false(cache->get(cache, req)); + } + g_assert_cmpint(cache->get_n_obj(cache), ==, 100); + + // hits on the main cache until the 1000th request + for (int i = 0; i < 899; i++) { + req->obj_id = 2 + i % 98; + g_assert_true(cache->get(cache, req)); + } + + // the main cache is now 95 objects instead of 99 + g_assert_cmpint(cache->get_n_obj(cache), ==, 96); + + // the window grows into the free space + for (obj_id_t id = 102; id <= 105; id++) { + req->obj_id = id; + g_assert_false(cache->get(cache, req)); + } + g_assert_cmpint(cache->get_n_obj(cache), ==, 100); + + free_request(req); + cache->cache_free(cache); +} + +// A period without hits must not trap the climber: 30-byte objects fit neither +// the 1% nor the 5% window of a 400-byte cache, so it must grow it to 10%. +static void test_WTinyLFU_hill_climb_no_hits(void) { + common_cache_params_t cc_params = {.cache_size = 400, .hashpower = 8}; + cache_t *cache = WTinyLFU_init(cc_params, "main-cache=LRU,hill-climb=1"); + request_t *req = new_request(); + req->obj_size = 30; + + int64_t n_hit = 0; + for (int i = 0; i < 1000; i++) { + req->obj_id = 1 + i % 5; + bool hit = cache->get(cache, req); + // a period is 134 requests; the window holds an object from the third on + if (i < 268) g_assert_false(hit); + n_hit += hit; + } + g_assert_cmpint(n_hit, >, 0); + + free_request(req); + cache->cache_free(cache); +} + +// SLRU_resize keeps the 1:4 shares: the first segment, which caps the object +// size, is 14 at size 71, 6 at size 30, and 14 again back at size 71. +static void test_SLRU_resize(void) { + common_cache_params_t cc_params = {.cache_size = 71, .hashpower = 8}; + cache_t *cache = SLRU_init(cc_params, "seg-size=1:4"); + request_t *req = new_request(); + req->obj_id = 1; + + SLRU_resize(cache, 30); + g_assert_cmpint(cache->cache_size, ==, 30); + req->obj_size = 6; + g_assert_true(cache->can_insert(cache, req)); + req->obj_size = 7; + g_assert_false(cache->can_insert(cache, req)); + + SLRU_resize(cache, 71); + req->obj_size = 14; + g_assert_true(cache->can_insert(cache, req)); + req->obj_size = 15; + g_assert_false(cache->can_insert(cache, req)); + + free_request(req); + cache->cache_free(cache); +} + +// After SLRU_resize an object can be larger than the next segment. A hit must +// keep it, not promote it, cool it back down and evict it. +static void test_SLRU_resize_keeps_large_object(void) { + common_cache_params_t cc_params = {.cache_size = 100, .hashpower = 8}; + cache_t *cache = SLRU_init(cc_params, "seg-size=1:4"); + request_t *req = new_request(); + req->obj_id = 1; + req->obj_size = 20; + + g_assert_false(cache->get(cache, req)); + SLRU_resize(cache, 20); // segments of 4 and 16 bytes + g_assert_true(cache->get(cache, req)); + g_assert_nonnull(cache->find(cache, req, false)); + + free_request(req); + cache->cache_free(cache); +} + /* The MINISIM profiler only knows an algorithm by its name and reaches it * through create_cache_using_plugin(). That used to go straight to dlsym(), * which cannot see a built-in constructor in a statically linked build — the @@ -725,6 +825,13 @@ int main(int argc, char *argv[]) { test_Clock2QPlus); g_test_add_func("/libCacheSim/cacheAlgo_WTinyLFU_counts_window_hits", test_WTinyLFU_counts_window_hits); + g_test_add_func("/libCacheSim/cacheAlgo_WTinyLFU_hill_climb_grows_window", + test_WTinyLFU_hill_climb_grows_window); + g_test_add_func("/libCacheSim/cacheAlgo_WTinyLFU_hill_climb_no_hits", + test_WTinyLFU_hill_climb_no_hits); + g_test_add_func("/libCacheSim/cacheAlgo_SLRU_resize", test_SLRU_resize); + g_test_add_func("/libCacheSim/cacheAlgo_SLRU_resize_keeps_large_object", + test_SLRU_resize_keeps_large_object); #if defined(ENABLE_3L_CACHE) && ENABLE_3L_CACHE == 1 g_test_add_data_func("/libCacheSim/cacheAlgo_3LCache", reader, test_3LCache);