From e6336967bc9359b460017c22f328bfc66d2fc9a6 Mon Sep 17 00:00:00 2001 From: Mukund Date: Sat, 28 Feb 2026 17:29:53 -0500 Subject: [PATCH 1/4] rough cache.rs, syntax is not totally there, added mainmemory file --- src/core/cache.rs | 128 +++++++++++++++++++++++++++++++++++++++++ src/core/mainmemory.rs | 3 + 2 files changed, 131 insertions(+) create mode 100644 src/core/mainmemory.rs diff --git a/src/core/cache.rs b/src/core/cache.rs index e69de29..1cdd259 100644 --- a/src/core/cache.rs +++ b/src/core/cache.rs @@ -0,0 +1,128 @@ +use std::collections::HashMap; + +// block size +const block_size: u32 = 4; + +// equivalent to block +pub struct CacheLine { + pub valid: bool, // if the data is valid + pub dirty: bool, // this is for a non-write through cache which we haven't implemented yet lmao + pub tag: u32, + pub index: u32, + pub size: u32, + pub data: [u8; BLOCKSIZE] +} + +pub struct Set { + pub lines: HashMap +} + +// here we write back to MM see comment below this is why we need MM +pub struct WriteThroughCache { + pub block_size: u32, + pub num_ways: u32, + pub num_sets: u32, + pub policy: Policy, + pub sets: [Set; num_sets] +} + +/* +we lowkey need main memory so that we know what + blocks to fetch when we have a cache miss + + theoretically when new data is created the whole thing should be written to main memory, but that will + happen via cache i guess? +*/ + +pub impl WriteThroughCache { + fn new(block_size : u32, num_ways: u32, num_sets : u32, policy: Policy) -> Self { + Self { + sets: [Set; num_sets] = std::array::from_fn(|_| + Set { + lines: HashMap::with_capacity(num_ways) + }); + block_size, num_ways, num_sets, policy + } + } + + + fn miss(address) {}{ + u32 offset_bits = block_size.ilog2(); + u32 index_bits = num_sets.ilog2(); + u32 tag_bits = 32 - offset_bits - index_bits; + + u32 offset_mask = block_size - 1; + u32 index_mask = (num_sets - 1) << offset_bits; + u32 tag_mask = (-1) << (offset_bits + index_bits); + + u32 offset = address & offset_mask; + u32 index = address & index_mask >> offset_bits; + u32 tag = address & tag_mask >> (offset_bits + index_bits); + + // maybe we also need to tell policy that we had a miss on a store vs a load + evict_choice = POLICY.evict_candidate(sets[index]) // policy tells us which one to evict out of the stuff in the sets + + + // create the new block + let new_line = CacheLine:: { + valid: false, + dirty: false, + tag: tag, + index: index, + size: block_size, + data: [u8; 64] + } + + + // now we pull the entire corresponding block for the missed address + u32 offset_bits = block_size.ilog2() + u32 block_address = (address >> offset_bits) << offset_bits + for index in 0..block_size { + new_line.data[index] = MAINMEMORY[(block_address << offset_bits) | index] + } + + sets[index][evict_choice] = new_line; + } + + + fn find(address : u32) -> Option { + u32 offset_bits = block_size.ilog2(); + u32 index_bits = num_sets.ilog2(); + u32 tag_bits = 32 - offset_bits - index_bits; + + u32 offset_mask = block_size - 1; + u32 index_mask = (num_sets - 1) << offset_bits; + u32 tag_mask = (0xFFFFFFFF) << (offset_bits + index_bits); + + u32 offset = address & offset_mask; + u32 index = address & index_mask >> offset_bits; + u32 tag = address & tag_mask >> (offset_bits + index_bits); + + if tag in sets[index] { + return Some sets[index][tag].data[offset] + } + return None + } + + fn read(address : u32) -> u8 { + match find(address) { + Some(byte) => byte // on read, we good. MIGHT NEED TO TELL POLICY WE HAD A HIT + None => { // store if needed, then return from mem + miss(address); + mem[address]; + } + } + } + + fn write(address : u32, val : u8) -> { + mem[address] = val; // store always + match find(address) { + Some(byte) => byte { // on hit, we st + sets[index][tag].data[offset] = val; // MIGHT NEED TO TELL POLICY WE HAD A HIT + } + None => { + miss(address) + } + } + } +} \ No newline at end of file diff --git a/src/core/mainmemory.rs b/src/core/mainmemory.rs new file mode 100644 index 0000000..73b415d --- /dev/null +++ b/src/core/mainmemory.rs @@ -0,0 +1,3 @@ +pub struct MainMemory { + pub mem[u8; SIZE] +} \ No newline at end of file From e3e626dbb9abf69b8f2c52bef1b6eb1c0fa2dd05 Mon Sep 17 00:00:00 2001 From: Shawn Zou Date: Sat, 7 Mar 2026 15:59:45 -0500 Subject: [PATCH 2/4] Add preliminary cache impl and cache test cases --- src/core/cache.rs | 169 ++++++++++----------------------- tests/cache_test.rs | 224 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 276 insertions(+), 117 deletions(-) diff --git a/src/core/cache.rs b/src/core/cache.rs index 1cdd259..28c5568 100644 --- a/src/core/cache.rs +++ b/src/core/cache.rs @@ -1,128 +1,63 @@ use std::collections::HashMap; -// block size -const block_size: u32 = 4; - -// equivalent to block -pub struct CacheLine { - pub valid: bool, // if the data is valid - pub dirty: bool, // this is for a non-write through cache which we haven't implemented yet lmao - pub tag: u32, - pub index: u32, - pub size: u32, - pub data: [u8; BLOCKSIZE] -} - -pub struct Set { - pub lines: HashMap +use crate::core::entry::Entry; +use crate::core::mainmemory::MainMemory; +use crate::core::metrics::Metrics; +use crate::core::policy::{CacheKey, Policy}; +use crate::core::time::Clock; + +pub struct Cache { + pub capacity: usize, + pub store: HashMap>, + pub policy: P, + pub metrics: Metrics, + pub clock: Clock, + pub main_memory: MainMemory, } -// here we write back to MM see comment below this is why we need MM -pub struct WriteThroughCache { - pub block_size: u32, - pub num_ways: u32, - pub num_sets: u32, - pub policy: Policy, - pub sets: [Set; num_sets] -} - -/* -we lowkey need main memory so that we know what - blocks to fetch when we have a cache miss - - theoretically when new data is created the whole thing should be written to main memory, but that will - happen via cache i guess? -*/ - -pub impl WriteThroughCache { - fn new(block_size : u32, num_ways: u32, num_sets : u32, policy: Policy) -> Self { - Self { - sets: [Set; num_sets] = std::array::from_fn(|_| - Set { - lines: HashMap::with_capacity(num_ways) - }); - block_size, num_ways, num_sets, policy +impl Cache { + pub fn new(capacity: usize, policy: P, main_memory: MainMemory) -> Self { + Self { + capacity, + store: HashMap::new(), + policy, + metrics: Metrics::new(), + clock: Clock::new(), + main_memory, } } - - fn miss(address) {}{ - u32 offset_bits = block_size.ilog2(); - u32 index_bits = num_sets.ilog2(); - u32 tag_bits = 32 - offset_bits - index_bits; - - u32 offset_mask = block_size - 1; - u32 index_mask = (num_sets - 1) << offset_bits; - u32 tag_mask = (-1) << (offset_bits + index_bits); - - u32 offset = address & offset_mask; - u32 index = address & index_mask >> offset_bits; - u32 tag = address & tag_mask >> (offset_bits + index_bits); - - // maybe we also need to tell policy that we had a miss on a store vs a load - evict_choice = POLICY.evict_candidate(sets[index]) // policy tells us which one to evict out of the stuff in the sets - - - // create the new block - let new_line = CacheLine:: { - valid: false, - dirty: false, - tag: tag, - index: index, - size: block_size, - data: [u8; 64] - } - - - // now we pull the entire corresponding block for the missed address - u32 offset_bits = block_size.ilog2() - u32 block_address = (address >> offset_bits) << offset_bits - for index in 0..block_size { - new_line.data[index] = MAINMEMORY[(block_address << offset_bits) | index] - } - - sets[index][evict_choice] = new_line; - } - - - fn find(address : u32) -> Option { - u32 offset_bits = block_size.ilog2(); - u32 index_bits = num_sets.ilog2(); - u32 tag_bits = 32 - offset_bits - index_bits; - - u32 offset_mask = block_size - 1; - u32 index_mask = (num_sets - 1) << offset_bits; - u32 tag_mask = (0xFFFFFFFF) << (offset_bits + index_bits); - - u32 offset = address & offset_mask; - u32 index = address & index_mask >> offset_bits; - u32 tag = address & tag_mask >> (offset_bits + index_bits); - - if tag in sets[index] { - return Some sets[index][tag].data[offset] - } - return None - } - - fn read(address : u32) -> u8 { - match find(address) { - Some(byte) => byte // on read, we good. MIGHT NEED TO TELL POLICY WE HAD A HIT - None => { // store if needed, then return from mem - miss(address); - mem[address]; + pub fn access(&mut self, key: CacheKey) { + self.clock.tick_up(); + let tick = self.clock.get_tick(); + + if let Some(entry) = self.store.get_mut(&key) { + // Hit + entry.on_access(tick); + self.policy.on_hit(key); + self.metrics.record_hit(); + } else { + // Miss + self.metrics.record_miss(); + + // If at capacity, ask policy who to evict + if self.store.len() >= self.capacity { + if let Some(evict_key) = self.policy.on_miss(key) { + self.store.remove(&evict_key); + self.policy.remove(evict_key); + self.metrics.record_eviction(); + } + } else { + self.policy.on_miss(key); } - } - } - fn write(address : u32, val : u8) -> { - mem[address] = val; // store always - match find(address) { - Some(byte) => byte { // on hit, we st - sets[index][tag].data[offset] = val; // MIGHT NEED TO TELL POLICY WE HAD A HIT - } - None => { - miss(address) - } + // Fetch from main memory + let _ = self.main_memory.fetch(key); + + // Insert new entry + let entry = Entry::new(key, 1, tick); + self.store.insert(key, entry); + self.policy.insert(key); } } -} \ No newline at end of file +} diff --git a/tests/cache_test.rs b/tests/cache_test.rs index e69de29..7c09ee0 100644 --- a/tests/cache_test.rs +++ b/tests/cache_test.rs @@ -0,0 +1,224 @@ +use lbce::core::cache::Cache; +use lbce::core::mainmemory::MainMemory; +use lbce::policies::fifo::Fifo; +use lbce::policies::lru::Lru; + +fn fifo_cache(capacity: usize) -> Cache { + Cache::new(capacity, Fifo::new(), MainMemory::new()) +} + +fn lru_cache(capacity: usize) -> Cache { + Cache::new(capacity, Lru::new(), MainMemory::new()) +} + +// --- Clock / time --- + +#[test] +fn clock_advances_each_access() { + let mut cache = fifo_cache(4); + assert_eq!(cache.clock.get_tick(), 0); + cache.access(1); + assert_eq!(cache.clock.get_tick(), 1); + cache.access(2); + assert_eq!(cache.clock.get_tick(), 2); + cache.access(1); // hit + assert_eq!(cache.clock.get_tick(), 3); +} + +// --- Entry --- + +#[test] +fn entry_insertion_tick_is_set_on_miss() { + let mut cache = fifo_cache(4); + cache.access(10); // tick becomes 1, entry inserted at tick 1 + let entry = cache.store.get(&10).unwrap(); + assert_eq!(entry.insertion_tick, 1); + assert_eq!(entry.last_access_tick, 1); + assert_eq!(entry.access_count, 1); +} + +#[test] +fn entry_updates_on_hit() { + let mut cache = fifo_cache(4); + cache.access(10); // tick=1, inserted + cache.access(10); // tick=2, hit + cache.access(10); // tick=3, hit + let entry = cache.store.get(&10).unwrap(); + assert_eq!(entry.insertion_tick, 1); + assert_eq!(entry.last_access_tick, 3); + assert_eq!(entry.access_count, 3); +} + +#[test] +fn entry_frequency_decreases_over_time() { + let mut cache = fifo_cache(4); + cache.access(5); // tick=1, access_count=1, freq = 1/(1-1+1) = 1.0 + cache.access(5); // tick=2, access_count=2, freq = 2/(2-1+1) = 1.0 + cache.access(6); // tick=3, miss — key 5 not accessed this tick + // key 5: access_count=2, insertion_tick=1, freq at tick=3 => 2/(3-1+1) = 2/3 + let entry = cache.store.get(&5).unwrap(); + let freq = entry.frequency(cache.clock.get_tick()); + assert!((freq - 2.0 / 3.0).abs() < 1e-9); +} + +// --- Metrics --- + +#[test] +fn metrics_all_misses() { + let mut cache = fifo_cache(4); + cache.access(1); + cache.access(2); + cache.access(3); + assert_eq!(cache.metrics.hit_count, 0); + assert_eq!(cache.metrics.request_count, 3); + assert_eq!(cache.metrics.hit_rate(), 0.0); + assert_eq!(cache.metrics.miss_rate(), 1.0); +} + +#[test] +fn metrics_mixed_hits_and_misses() { + let mut cache = fifo_cache(4); + cache.access(1); // miss + cache.access(2); // miss + cache.access(1); // hit + cache.access(2); // hit + assert_eq!(cache.metrics.request_count, 4); + assert_eq!(cache.metrics.hit_count, 2); + assert_eq!(cache.metrics.hit_rate(), 0.5); + assert_eq!(cache.metrics.miss_rate(), 0.5); +} + +#[test] +fn metrics_eviction_count() { + let mut cache = fifo_cache(2); + cache.access(1); + cache.access(2); + cache.access(3); // eviction + cache.access(4); // eviction + assert_eq!(cache.metrics.eviction_count, 2); +} + +#[test] +fn metrics_empty_cache_hit_rate_is_zero() { + let cache = fifo_cache(4); + assert_eq!(cache.metrics.hit_rate(), 0.0); +} + +// --- FIFO policy --- + +#[test] +fn fifo_hit_on_second_access() { + let mut cache = fifo_cache(4); + cache.access(1); // miss + cache.access(1); // hit + assert_eq!(cache.metrics.hit_count, 1); +} + +#[test] +fn fifo_no_eviction_below_capacity() { + let mut cache = fifo_cache(3); + cache.access(1); + cache.access(2); + cache.access(3); + assert_eq!(cache.metrics.eviction_count, 0); + assert_eq!(cache.store.len(), 3); +} + +#[test] +fn fifo_evicts_oldest_inserted() { + let mut cache = fifo_cache(2); + cache.access(1); // oldest + cache.access(2); + cache.access(3); // at capacity: evict 1 + assert!(!cache.store.contains_key(&1), "FIFO must evict key 1 (oldest)"); + assert!(cache.store.contains_key(&2)); + assert!(cache.store.contains_key(&3)); +} + +#[test] +fn fifo_hit_does_not_protect_key_from_eviction() { + // FIFO never reorders; hitting key 1 doesn't save it + let mut cache = fifo_cache(2); + cache.access(1); // inserted first + cache.access(2); + cache.access(1); // hit — FIFO doesn't move 1 + cache.access(3); // still evicts 1 (oldest inserted) + assert!(!cache.store.contains_key(&1), "FIFO evicts by insertion order, ignoring hits"); + assert!(cache.store.contains_key(&2)); + assert!(cache.store.contains_key(&3)); +} + +#[test] +fn fifo_eviction_sequence_is_insertion_order() { + let mut cache = fifo_cache(2); + cache.access(1); + cache.access(2); + cache.access(3); // evict 1 + cache.access(4); // evict 2 + assert!(!cache.store.contains_key(&1)); + assert!(!cache.store.contains_key(&2)); + assert!(cache.store.contains_key(&3)); + assert!(cache.store.contains_key(&4)); + assert_eq!(cache.metrics.eviction_count, 2); +} + +// --- LRU policy --- + +#[test] +fn lru_hit_on_second_access() { + let mut cache = lru_cache(4); + cache.access(1); // miss + cache.access(1); // hit + assert_eq!(cache.metrics.hit_count, 1); +} + +#[test] +fn lru_no_eviction_below_capacity() { + let mut cache = lru_cache(3); + cache.access(1); + cache.access(2); + cache.access(3); + assert_eq!(cache.metrics.eviction_count, 0); + assert_eq!(cache.store.len(), 3); +} + +#[test] +fn lru_without_hits_evicts_oldest() { + // No hits means insertion order == recency order, same as FIFO + let mut cache = lru_cache(2); + cache.access(1); + cache.access(2); + cache.access(3); // evict 1 (least recently used) + assert!(!cache.store.contains_key(&1)); + assert!(cache.store.contains_key(&2)); + assert!(cache.store.contains_key(&3)); +} + +#[test] +fn lru_hit_protects_key_from_eviction() { + let mut cache = lru_cache(2); + cache.access(1); // insert 1 (LRU) + cache.access(2); // insert 2 (MRU) + cache.access(1); // hit — 1 becomes MRU, 2 becomes LRU + cache.access(3); // evict 2 (now LRU) + assert!(cache.store.contains_key(&1), "LRU must keep key 1 (recently hit)"); + assert!(!cache.store.contains_key(&2), "LRU must evict key 2 (least recently used)"); + assert!(cache.store.contains_key(&3)); +} + +#[test] +fn lru_eviction_sequence_tracks_recency() { + let mut cache = lru_cache(3); + cache.access(1); + cache.access(2); + cache.access(3); + // order: [1(LRU), 2, 3(MRU)] + cache.access(2); // hit — order: [1(LRU), 3, 2(MRU)] + cache.access(4); // evict 1 + assert!(!cache.store.contains_key(&1)); + cache.access(5); // evict 3 + assert!(!cache.store.contains_key(&3)); + assert!(cache.store.contains_key(&2)); + assert!(cache.store.contains_key(&4)); + assert!(cache.store.contains_key(&5)); +} From c024311fe13a5a1e6428af04c94ff950a22f3d52 Mon Sep 17 00:00:00 2001 From: Mukund Date: Sat, 7 Mar 2026 16:15:21 -0500 Subject: [PATCH 3/4] update mainmemory.rs to be a large hashmap data store --- src/core/cache.rs | 2 +- src/core/mainmemory.rs | 21 ++++++++++++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/core/cache.rs b/src/core/cache.rs index 28c5568..4e96ad1 100644 --- a/src/core/cache.rs +++ b/src/core/cache.rs @@ -42,7 +42,7 @@ impl Cache { // If at capacity, ask policy who to evict if self.store.len() >= self.capacity { - if let Some(evict_key) = self.policy.on_miss(key) { + if let Some(evict_key) = self.policy.victim(key) { self.store.remove(&evict_key); self.policy.remove(evict_key); self.metrics.record_eviction(); diff --git a/src/core/mainmemory.rs b/src/core/mainmemory.rs index 73b415d..7f1c4dd 100644 --- a/src/core/mainmemory.rs +++ b/src/core/mainmemory.rs @@ -1,3 +1,22 @@ +use std::collections::HashMap; + +use crate::core::entry::Entry; +use crate::core::metrics::Metrics; +use crate::core::policy::{CacheKey, Policy}; +use crate::core::time::Clock; + pub struct MainMemory { - pub mem[u8; SIZE] + pub mem: HashMap>, +} + +impl MainMemory { + pub fn new() -> Self { + Self { + mem: HashMap::new() + } + } + + pub fn fetch(&self, key: &CacheKey) -> Option<&mut Entry>{ + self.mem.get(key) + } } \ No newline at end of file From d51a409ab5086b7766c4930cf6ef11ac3736fe48 Mon Sep 17 00:00:00 2001 From: Mukund Date: Sun, 8 Mar 2026 14:24:25 -0400 Subject: [PATCH 4/4] update on_miss/victim --- src/core/cache.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/core/cache.rs b/src/core/cache.rs index 4e96ad1..c4bfa0d 100644 --- a/src/core/cache.rs +++ b/src/core/cache.rs @@ -41,8 +41,12 @@ impl Cache { self.metrics.record_miss(); // If at capacity, ask policy who to evict + + /* on_miss and victim are interchangeable ig? and then remove is the thing that tells the policy that we + did actually get rid of something + */ if self.store.len() >= self.capacity { - if let Some(evict_key) = self.policy.victim(key) { + if let Some(evict_key) = self.policy.on_miss(key) { self.store.remove(&evict_key); self.policy.remove(evict_key); self.metrics.record_eviction();