How to Build a Cache Simulator Using the C++ API of libCacheSim: A Complete Guide

libCacheSim provides a thin, function-pointer-based C API that integrates seamlessly with C++ to build high-performance cache simulators using only three core components: a trace reader, a cache object, and a request handler.

libCacheSim is a high-performance caching library designed for large-scale trace-driven simulation. While written in C, its API is fully compatible with C++ and allows you to build a cache simulator using the C++ API of libCacheSim with minimal overhead. The library exposes three essential building blocks—the trace reader, the cache instance, and the request handler—that form the foundation of any simulation workflow.

Core Components of a libCacheSim Simulator

Building a simulator requires understanding three interconnected components that manage the simulation lifecycle.

Trace Reader

The trace reader parses trace files (CSV, VSCSI, binary, or others) and yields request_t objects. The core functions open_trace(), read_one_req(), and close_trace() are defined in libCacheSim/traceReader/reader.c. These functions handle file I/O, format detection, and deserialization of request metadata such as object IDs, sizes, and timestamps.

Cache Object

The cache object maintains eviction state using algorithms like LRU, LFU, or S3-FIFO. Creation occurs through init functions such as LRU_init(), declared in libCacheSim/include/libCacheSim/evictionAlgo.h and implemented in algorithm-specific files like libCacheSim/cache/eviction/LRU.c. These functions return a cache_t* pointer containing function pointers for get, insert, and evict operations.

Request Handler

The request handler orchestrates the simulation loop. The function new_request() allocates a reusable request_t structure defined in libCacheSim/include/libCacheSim.h. During simulation, cache->get() serves as the high-level interface, internally invoking find, insert, and evict as needed while updating internal counters like cache->n_req and virtual time.

Step-by-Step Implementation Guide

Follow these steps to implement a single-threaded cache simulator in C++.

1. Include the Public Header

Begin by including the main header file that provides type definitions and function prototypes.

#include <libCacheSim.h>

2. Configure the Trace Reader

Initialize a reader for your specific trace format. For CSV files, populate a reader_init_param_t structure to map columns to request fields.

reader_init_param_t csv_params = {
    .delimiter = ',',
    .time_field = 2,
    .obj_id_field = 5,
    .obj_size_field = 4,
    .has_header = TRUE,
    .has_header_set = TRUE,
    .obj_id_is_num = TRUE
};
reader_t *reader = open_trace("../data/trace.csv", CSV_TRACE, &csv_params);

3. Allocate a Request Object

Create a single request object that you reuse across all iterations to minimize allocation overhead.

request_t *req = new_request();

4. Initialize the Cache

Set cache parameters using common_cache_params_t and instantiate your chosen eviction algorithm. The example below configures a 1 GiB LRU cache with 2^16 hash buckets.

common_cache_params_t cc = default_common_cache_params();
cc.cache_size = 1ULL * GiB;
cc.hashpower = 16;

cache_t *cache = LRU_init(cc, nullptr);

5. Execute the Simulation Loop

Drive the simulation by reading requests sequentially and querying the cache. The cache->get() method returns true for hits and false for misses.

uint64_t n_req = 0, n_miss = 0;
while (read_one_req(reader, req) == 0) {
    if (!cache->get(cache, req))
        ++n_miss;
    ++n_req;
}

6. Report Results and Cleanup

Calculate statistics and release allocated resources to prevent memory leaks.

printf("Miss ratio = %.4f%%\n", 100.0 * (double)n_miss / (double)n_req);

cache->cache_free(cache);
close_trace(reader);
free_request(req);

Complete Working Example

Below is a complete, compilable C++ program that demonstrates the full simulation pipeline.

#include <libCacheSim.h>
#include <cstdio>

int main() {
    /* ---- 1. open a CSV trace ----------------------------------- */
    reader_init_param_t csv_params = {
        .delimiter = ',',
        .time_field = 2,
        .obj_id_field = 5,
        .obj_size_field = 4,
        .has_header = TRUE,
        .has_header_set = TRUE,
        .obj_id_is_num = TRUE
    };
    reader_t *r = open_trace("../data/trace.csv", CSV_TRACE, &csv_params);
    if (!r) { fprintf(stderr, "Cannot open trace\n"); return 1; }

    /* ---- 2. allocate a request object --------------------------- */
    request_t *req = new_request();

    /* ---- 3. create a cache (LRU) ------------------------------ */
    common_cache_params_t cc = default_common_cache_params();
    cc.cache_size = 512ULL * MiB;   // 512 MiB
    cc.hashpower  = 16;
    cache_t *c = LRU_init(cc, nullptr);

    /* ---- 4. simulation loop ----------------------------------- */
    uint64_t n_req = 0, n_miss = 0;
    while (read_one_req(r, req) == 0) {
        if (!c->get(c, req)) ++n_miss;
        ++n_req;
    }

    /* ---- 5. report & cleanup --------------------------------- */
    printf("Requests: %lu, Misses: %lu, Miss ratio: %.4f%%\n",
           n_req, n_miss,
           100.0 * (double)n_miss / (double)n_req);

    c->cache_free(c);
    close_trace(r);
    free_request(req);
    return 0;
}

Multi-Threaded and Bulk Simulation

For large-scale experiments, libCacheSim provides bulk simulation APIs that parallelize execution. The simulate_at_multi_sizes() function, declared in libCacheSim/include/libCacheSim/simulator.h, launches multiple threads—each running an independent cache instance across different size configurations—and returns aggregated statistics as cache_stat_t structures.

The repository includes a comprehensive example in example/cacheSimulatorConcurrent/main.cpp demonstrating how to leverage these concurrent capabilities for throughput-intensive workloads.

Build Instructions

Compile your simulator using CMake and Ninja, linking against libCacheSim and its dependencies (glib, zstd).

mkdir _build && cd _build
cmake -G Ninja ..
ninja
./my_cache_sim

Summary

  • libCacheSim exposes a C-compatible API that works natively with C++ through three core abstractions: the trace reader (open_trace, read_one_req), the cache object (LRU_init, cache->get), and the request handler (new_request).
  • The simulation loop follows a simple pattern: read requests sequentially, invoke cache->get() to check for hits or misses, and accumulate statistics.
  • Resource cleanup requires calling cache->cache_free(), close_trace(), and free_request() to release internal hash tables and file handles.
  • For high-throughput scenarios, use the bulk simulation APIs in libCacheSim/include/libCacheSim/simulator.h and refer to example/cacheSimulatorConcurrent/main.cpp for multi-threaded implementations.

Frequently Asked Questions

How do I switch from LRU to a different eviction algorithm?

Replace LRU_init() with the corresponding init function declared in libCacheSim/include/libCacheSim/evictionAlgo.h. For example, use LFU_init() for Least Frequently Used or S3FIFO_init() for the S3-FIFO algorithm. All init functions accept the same common_cache_params_t structure and return a cache_t* pointer with the same interface.

Can I use libCacheSim with C++ smart pointers or RAII?

While libCacheSim returns raw pointers (cache_t*, reader_t*), you can wrap them in std::unique_ptr with custom deleters. For instance, use std::unique_ptr<cache_t, decltype(&cache_free)> to ensure automatic cleanup, though you must reference the specific free functions (cache_free, close_trace, free_request) as deleters rather than direct destructors.

What trace formats does libCacheSim support?

The library supports CSV, VSCSI, binary, and several other formats through the trace_type parameter in open_trace(). The dispatcher in libCacheSim/traceReader/reader.c automatically selects the appropriate parser based on this enum value, allowing you to process standard research traces without manual preprocessing.

How does cache->get() handle misses internally?

According to the implementation in libCacheSim/cache/eviction/LRU.c, cache->get() wraps cache_get_base(), which first calls find() to check object presence. On a miss, it may invoke evict() if the cache is full, followed by insert() to admit the new object. This entry point automatically updates cache->n_req and internal virtual time counters, ensuring consistent statistics across all algorithms.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →