Skip to content

Commit 4d183aa

Browse files
haochengxiaclaude
andauthored
feat: Add S4-FIFO algo (#335)
* feat: Add S4-FIFO algo * fix(s4fifo): address review findings on child sizing, warmup and feature tracking Five fixes from the review of the S4-FIFO PR: - Size each sub-FIFO's hash table from its own byte size, capped by any requested --hashpower, rather than inheriting the parent's. Previously a single S4FIFO allocated three full-size tables; at a 1GiB cache size peak RSS drops from 527MiB to 338MiB, matching S3FIFO's 340MiB, with an identical miss ratio. - Leave WARMUP on the first capacity eviction instead of when occupancy reaches cache_size. Eviction runs before occupancy can exceed the limit, so the old test needed the trace to fill the cache exactly and in practice never fired: on cloudPhysicsIO the learned mode spent the whole trace in warmup and never predicted a configuration. - Only pre-scan the trace for its request count when an algorithm actually reads it. Every cachesim and MRC run paid for a full extra pass, which on a txt or zstd trace costs as much as the simulation (LRU on a 1M-request csv: 0.48s -> 0.33s). - Stop double-counting one-hit-wonders: S4FIFO_track_ghost_insert already records them, so the extra call doubled one_hit_count whenever a ghost FIFO was configured. - Stamp clock reinsertions in the main FIFO with S4FIFO_track_main_insert. A reinserted object takes a fresh tail position but kept a stale insert_seq, skewing the main queue's hit-position histogram. The eviction path itself is unchanged: the static heuristic still matches S3-FIFO exactly, and the full test suite passes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(s4fifo): propagate n_total_req through cloning, and fix ghost-queue tracking Second round of review fixes. - Adding n_total_req grew common_cache_params_t from 24 to 32 bytes, but clone_cache and create_cache_with_new_size still asserted the old size, so any assertion-enabled build aborted in simulate_at_multi_sizes (testSimulator exited 134 under -DCMAKE_BUILD_TYPE=Debug). That assert was a tripwire for exactly the bug that had happened - both functions rebuild the struct field by field and were silently dropping the new field - so copy n_total_req in both, retain it on cache_t to copy it from, and make the tripwire a _Static_assert. As a runtime assert it was compiled out of release builds and so never fired in CI; now it breaks the build for everyone, which is the point. Verified it still trips by adding a scratch field. - MINISIM hardcoded n_total_req = 0 even though it had just counted the trace, so the default fractional feature-collect-reqs fell back to a fixed 10000 and MRC disagreed with cachesim. Pass sampled_cnt - not n_req_, since spatial sampling drops requests before the cache sees them, so sampled_cnt is what cache->n_req climbs to. On twitter_cluster52 at wss 0.055 the miss ratio goes 0.216370 -> 0.205729, and the curve now matches cachesim exactly (0.299438 and 0.185243 against 0.2994 and 0.1852). - S4FIFO_track_ghost_insert returned early outside a collection window, so objects inserted during warmup or between windows kept a zero insert_seq and ghost_insert_seq froze. A later hit inside a window was then scored against a stamp from a different sequence. Always stamp and advance, as the small and main helpers do, and gate only the per-window one-hit counter. Costs ~1% throughput (4.82 -> 4.78 MQPS) for the extra lookup. - record_ghost_removal ran on every ghost hit, but with a positive ghost-to-main-threshold the entry deliberately stays in the queue. The tracker subtracts recorded removals as holes from later hit positions, so this invented holes that were never there; record it only in the branch that actually removes. Release and Debug both build clean and pass all 10 tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 9fc2edd commit 4d183aa

24 files changed

Lines changed: 90567 additions & 20 deletions

CMakeLists.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ option(SUPPORT_TTL "whether support TTL" OFF)
2626
option(OPT_SUPPORT_ZSTD_TRACE "whether support zstd trace" ON)
2727
option(ENABLE_LRB "enable LRB" OFF)
2828
option(ENABLE_3L_CACHE "enable 3LCache" OFF)
29+
option(ENABLE_S4FIFO "enable S4FIFO" OFF)
2930
option(BUILD_SHARED_LIBS "build shared library" ON)
3031
set(LOG_LEVEL "default" CACHE STRING "change the logging level")
3132
set_property(CACHE LOG_LEVEL PROPERTY STRINGS ERROR WARN INFO DEBUG VERBOSE DEFAULT)
@@ -192,6 +193,11 @@ foreach(FEATURE ENABLE_LRB ENABLE_3L_CACHE)
192193
add_compile_definitions(${FEATURE}=1)
193194
endif()
194195
endforeach()
196+
197+
if(ENABLE_S4FIFO)
198+
add_compile_definitions(ENABLE_S4FIFO=1)
199+
endif()
200+
195201
# Put binary in bin directory
196202
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
197203

@@ -202,6 +208,7 @@ message(STATUS "CMAKE_CXX_FLAGS_RELEASE ${CMAKE_CXX_FLAGS_RELEASE}")
202208
message(STATUS "SUPPORT TTL ${SUPPORT_TTL}, USE_HUGEPAGE ${USE_HUGEPAGE}")
203209
message(STATUS "LOGLEVEL ${LOG_LEVEL}, ENABLE_GLCACHE ${ENABLE_GLCACHE}")
204210
message(STATUS "ENABLE_LRB ${ENABLE_LRB}, ENABLE_3L_CACHE ${ENABLE_3L_CACHE}")
211+
message(STATUS "ENABLE_S4FIFO ${ENABLE_S4FIFO}")
205212
message(STATUS "OPT_SUPPORT_ZSTD_TRACE ${OPT_SUPPORT_ZSTD_TRACE}")
206213

207214
message(STATUS "<<++=====------------------\\/------------------=====++>>")

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ The name in `code` is what you pass to `cachesim` on the command line (names are
6565
* [WTinyLFU](/libCacheSim/cache/eviction/WTinyLFU.c) `wtinylfu`
6666
* [QD-LP](/libCacheSim/cache/eviction/QDLP.c) `qdlp`
6767
* [S3-FIFO](/libCacheSim/cache/eviction/S3FIFO.c) `s3fifo`, [S3-FIFOd](/libCacheSim/cache/eviction/S3FIFOd.c) `s3fifod`
68+
* [S4-FIFO](/libCacheSim/cache/eviction/S4FIFO/S4FIFO.c) `s4fifo` — build with `-DENABLE_S4FIFO=ON`
6869
* [Sieve](/libCacheSim/cache/eviction/Sieve.c) `sieve`
6970

7071
### Admission algorithms

libCacheSim/bin/MRC/parser_mini.c

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -329,13 +329,25 @@ void parse_mini_cmd(int argc, char *argv[], struct MINI_arguments *args) {
329329
* the working set size **/
330330
conv_cache_sizes(args->args[4], args->args[5], args);
331331

332+
/* lets an eviction parameter be given as a fraction of the trace rather
333+
* than an absolute request count - see the same call in cachesim, which
334+
* also explains why only the algorithms that read it pay for the scan. */
335+
int64_t n_total_req = 0;
336+
for (int i = 0; i < args->n_eviction_algo; i++) {
337+
if (cache_needs_n_total_req(args->eviction_algo[i])) {
338+
n_total_req = get_num_of_req(args->reader);
339+
reset_reader(args->reader);
340+
break;
341+
}
342+
}
343+
332344
for (int i = 0; i < args->n_eviction_algo; i++) {
333345
for (int j = 0; j < args->n_cache_size; j++) {
334346
int idx = i * args->n_cache_size + j;
335-
args->caches[idx] =
336-
create_cache(args->trace_path, args->eviction_algo[i],
337-
args->cache_sizes[j], args->eviction_params,
338-
args->consider_obj_metadata, DEFAULT_HASHPOWER);
347+
args->caches[idx] = create_cache(
348+
args->trace_path, args->eviction_algo[i], args->cache_sizes[j],
349+
args->eviction_params, args->consider_obj_metadata, DEFAULT_HASHPOWER,
350+
n_total_req);
339351

340352
if (args->admission_algo != NULL) {
341353
args->caches[idx]->admissioner =

libCacheSim/bin/cachesim/cache_init.h

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,20 @@ extern "C" {
1717
/* log2 of the hash table size; 24 gives 16M entries */
1818
#define DEFAULT_HASHPOWER 24
1919

20+
/**
21+
* @brief does this algorithm read common_cache_params_t.n_total_req?
22+
*
23+
* Supplying it means counting the trace's requests before the simulation
24+
* starts, which on a txt or zstd trace is a full extra pass — as expensive as
25+
* the simulation itself. Only the algorithms listed here accept a parameter
26+
* expressed as a fraction of the trace (s4fifo's feature-collect-reqs, whose
27+
* default is fractional), so everything else should not pay for the pre-scan.
28+
*/
29+
static inline bool cache_needs_n_total_req(const char *eviction_algo) {
30+
return strcasecmp(eviction_algo, "s4fifo") == 0 ||
31+
strcasecmp(eviction_algo, "s4-fifo") == 0;
32+
}
33+
2034
/**
2135
* @brief create a cache for the CLI, given the algorithm name
2236
*
@@ -32,12 +46,14 @@ static inline cache_t *create_cache(const char *trace_path,
3246
const uint64_t cache_size,
3347
const char *eviction_params,
3448
const bool consider_obj_metadata,
35-
const int hashpower) {
49+
const int hashpower,
50+
const int64_t n_total_req) {
3651
common_cache_params_t cc_params = {
3752
.cache_size = cache_size,
3853
.default_ttl = 86400 * 300,
3954
.hashpower = hashpower,
4055
.consider_obj_metadata = consider_obj_metadata,
56+
.n_total_req = n_total_req,
4157
};
4258
cache_t *cache;
4359

libCacheSim/bin/cachesim/cli_parser.c

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -356,12 +356,29 @@ void parse_cmd(int argc, char *argv[], struct arguments *args) {
356356
* the working set size **/
357357
conv_cache_sizes(args->args[3], args);
358358

359+
/* so that an eviction parameter can be given as a fraction of the trace
360+
* (e.g. s4fifo's feature-collect-reqs) rather than an absolute request
361+
* count. Cheap for binary traces (derived from the file size); for txt
362+
* and zstd traces it counts on a cloned reader, leaving this one
363+
* untouched - reset anyway so nothing downstream depends on that. Only
364+
* the algorithms that read it pay for the scan; for everything else it
365+
* would be a wasted pass over the whole trace. */
366+
int64_t n_total_req = 0;
367+
for (int i = 0; i < args->n_eviction_algo; i++) {
368+
if (cache_needs_n_total_req(args->eviction_algo[i])) {
369+
n_total_req = get_num_of_req(args->reader);
370+
reset_reader(args->reader);
371+
break;
372+
}
373+
}
374+
359375
for (int i = 0; i < args->n_eviction_algo; i++) {
360376
for (int j = 0; j < args->n_cache_size; j++) {
361377
int idx = i * args->n_cache_size + j;
362378
args->caches[idx] = create_cache(
363379
args->trace_path, args->eviction_algo[i], args->cache_sizes[j],
364-
args->eviction_params, args->consider_obj_metadata, args->hashpower);
380+
args->eviction_params, args->consider_obj_metadata, args->hashpower,
381+
n_total_req);
365382

366383
if (args->admission_algo != NULL) {
367384
args->caches[idx]->admissioner =

libCacheSim/bin/traceUtils/traceFilterMain.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,8 @@ int main(int argc, char *argv[]) {
7878
.cache_size = static_cast<uint64_t>(args.cache_size),
7979
.default_ttl = 86400 * 300,
8080
.hashpower = 24,
81-
.consider_obj_metadata = false};
81+
.consider_obj_metadata = false,
82+
.n_total_req = 0};
8283

8384
if (strcasecmp(args.cache_name, "LRU") == 0) {
8485
args.cache = LRU_init(cc_params, NULL);

libCacheSim/cache/CMakeLists.txt

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,12 +92,22 @@ endif()
9292

9393
# 3L Cache
9494
if (ENABLE_3L_CACHE)
95-
set (eviction_sources_cpp ${eviction_sources_cpp}
95+
set (eviction_sources_cpp ${eviction_sources_cpp}
9696
eviction/3LCache/ThreeLCache_Interface.cpp
9797
eviction/3LCache/ThreeLCache.cpp
9898
)
9999
endif()
100100

101+
# S4FIFO: plain C throughout, including its model, so no ML runtime
102+
# dependency and no eviction_sources_cpp entry.
103+
if (ENABLE_S4FIFO)
104+
set(eviction_sources_c ${eviction_sources_c}
105+
eviction/S4FIFO/S4FIFO.c
106+
eviction/S4FIFO/S4FIFO_predictor.c
107+
eviction/S4FIFO/S4FIFO_model_real.c
108+
)
109+
endif()
110+
101111
# GLCache
102112
if (ENABLE_GLCACHE)
103113
set(eviction_sources_c

libCacheSim/cache/cache.c

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,21 @@ extern "C" {
1717
*LRU and FIFO
1818
**/
1919

20+
/* The two functions below rebuild a common_cache_params_t field by field, so
21+
* a field added to that struct is silently dropped unless they are updated
22+
* too. This is the tripwire for that: it breaks the build when the struct
23+
* grows, pointing at the initializer that needs the new field.
24+
*
25+
* It used to be `assert(sizeof(cc_params) == 24)`, which caught nothing in a
26+
* release build (NDEBUG) and aborted every simulate_at_multi_sizes() run in a
27+
* debug one. A static assertion fires for everybody, at compile time, which is
28+
* what a tripwire like this is for. Update the expected size together with the
29+
* initializers. */
30+
#define CHECK_COMMON_CACHE_PARAMS_COPIED_ABOVE() \
31+
_Static_assert(sizeof(common_cache_params_t) == 32, \
32+
"common_cache_params_t changed size: copy the new " \
33+
"field in the initializer above, then update this size")
34+
2035
/**
2136
* @brief this function is called by all eviction algorithms to initialize the
2237
* cache
@@ -37,6 +52,7 @@ cache_t *cache_struct_init(const char *const cache_name,
3752
cache->init_params[CACHE_INIT_PARAMS_LEN - 1] = '\0';
3853
}
3954
cache->cache_size = params.cache_size;
55+
cache->n_total_req = params.n_total_req;
4056
cache->eviction_params = NULL;
4157
cache->admissioner = NULL;
4258
cache->prefetcher = NULL;
@@ -95,8 +111,9 @@ cache_t *clone_cache(const cache_t *old_cache) {
95111
.hashpower = old_cache->hashtable->hashpower,
96112
.default_ttl = old_cache->default_ttl,
97113
.consider_obj_metadata = old_cache->obj_md_size == 0 ? false : true,
114+
.n_total_req = old_cache->n_total_req,
98115
};
99-
assert(sizeof(cc_params) == 24);
116+
CHECK_COMMON_CACHE_PARAMS_COPIED_ABOVE();
100117
cache_t *cache = old_cache->cache_init(cc_params, old_cache->init_params);
101118
if (old_cache->admissioner != NULL) {
102119
cache->admissioner = old_cache->admissioner->clone(old_cache->admissioner);
@@ -121,8 +138,9 @@ cache_t *create_cache_with_new_size(const cache_t *old_cache,
121138
.hashpower = old_cache->hashtable->hashpower,
122139
.default_ttl = old_cache->default_ttl,
123140
.consider_obj_metadata = old_cache->obj_md_size == 0 ? false : true,
141+
.n_total_req = old_cache->n_total_req,
124142
};
125-
assert(sizeof(cc_params) == 24);
143+
CHECK_COMMON_CACHE_PARAMS_COPIED_ABOVE();
126144
cache_t *cache = old_cache->cache_init(cc_params, old_cache->init_params);
127145
if (old_cache->admissioner != NULL) {
128146
cache->admissioner = old_cache->admissioner->clone(old_cache->admissioner);

libCacheSim/cache/cacheAlgoRegistry.c

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,10 @@ static const cache_algo_entry_t g_cache_algos[] = {
6767
{"s3fifo", S3FIFO_init},
6868
{"s3fifod", S3FIFOd_init},
6969
{"s3fifov0", S3FIFOv0_init},
70+
#ifdef ENABLE_S4FIFO
71+
{"s4-fifo", S4FIFO_init},
72+
{"s4fifo", S4FIFO_init},
73+
#endif
7074
{"sieve", Sieve_init},
7175
{"size", Size_init},
7276
{"slru", SLRU_init},

0 commit comments

Comments
 (0)