diff --git a/include/gcache/ghost_cache.h b/include/gcache/ghost_cache.h index 3d61b15..e7f978d 100644 --- a/include/gcache/ghost_cache.h +++ b/include/gcache/ghost_cache.h @@ -29,17 +29,16 @@ class GhostCache { uint32_t min_size; uint32_t max_size; uint32_t num_ticks; - uint32_t lru_size; // Number of handles in cache LRU right now // Key is block_id/block number - // Value is "size_idx", which means the handle will in cache if the cache size - // is ((size_idx + 1) * tick) but not if the cache size is (size_idx * tick). + // Value is "size_idx", which is the least non-negative number such that the + // key will in cache if the cache size is (size_idx * tick) + min_size LRUCache cache; using Handle_t = typename LRUCache::Handle_t; using Node_t = typename LRUCache::Node_t; // these must be placed after num_ticks to ensure a correct ctor order - std::vector size_boundaries; + std::vector boundaries; std::vector caches_stat; void access_impl(uint32_t block_id, uint32_t hash, AccessMode mode); @@ -50,15 +49,16 @@ class GhostCache { min_size(min_size), max_size(max_size), num_ticks((max_size - min_size) / tick + 1), - lru_size(0), cache(), - size_boundaries(num_ticks, nullptr), + boundaries(num_ticks - 1, nullptr), caches_stat(num_ticks) { assert(tick > 0); assert(min_size > 1); // otherwise the first boundary will be LRU evicted assert(min_size + (num_ticks - 1) * tick == max_size); + assert(num_ticks > 2); cache.init(max_size); } + void access(uint32_t block_id, AccessMode mode = AccessMode::DEFAULT) { access_impl(block_id, Hash{}(block_id), mode); } @@ -87,6 +87,18 @@ class GhostCache { for (auto& s : caches_stat) s.reset(); } + // For each item in the LRU list, call fn(key) in LRU order + template + void for_each_lru(Fn&& fn) { + cache.for_each_lru([&fn](Handle_t h) { fn(h.get_key()); }); + } + + // For each item in the LRU list, call fn(key) in LRU order + template + void for_each_mru(Fn&& fn) { + cache.for_each_mru([&fn](Handle_t h) { fn(h.get_key()); }); + } + std::ostream& print(std::ostream& os, int indent = 0) const; friend std::ostream& operator<<(std::ostream& os, const GhostCache& c) { return c.print(os); @@ -145,47 +157,61 @@ class SampledGhostCache : public GhostCache { template inline void GhostCache::access_impl(uint32_t block_id, uint32_t hash, AccessMode mode) { - uint32_t size_idx; Handle_t s; // successor Handle_t h = cache.refresh(block_id, hash, s); assert(h); // Since there is no handle in use, allocation must never fail. /** - * To reason through the code below, consider an example where min_size=1, - * max_size=5, tick=2. - * DummyHead <=> A <=> B <=> C <=> D <=> E, where E is MRU and A is LRU. - * size_idx: 2, 1, 1, 0, 0. - * size_boundaries: [1] [0] + * To reason through the code below, consider an example where min_size=3, + * max_size=7, tick=2, num_ticks=3. + * (LRU) (MRU) + * DummyHead <=> A <=> B <=> C <=> D <=> E <=> F <=> G. + * size_idx: 2, 2, 1, 1, 0, 0, 0. + * boundaries: [1] [0] + * + * If B is accessed, the list becomes: + * DummyHead <=> A <=> C <=> D <=> E <=> F <=> G <=> B. + * size_idx: 2, 2, 1, 1, 0, 0, 0. + * idx_changed: ^ ^ ^ + * boundaries: [1] [0] * - * Now B is accessed, so the list becomes: - * DummyHead <=> A <=> C <=> D <=> E <=> B. - * size_idx: 2, 1, 1, 0, 0. - * size_boundaries: [1] [0] - * To get to this state, - * 1) boundaries with size_idx < B should move to its next and increase - * its size_idx - * 2) if B itself is a boundary, set that boundary to B's sucessor. + * If instead C is accessed, so the list becomes: + * DummyHead <=> A <=> B <=> D <=> E <=> F <=> G <=> C. + * size_idx: 2, 2, 1, 1, 0, 0, 0. + * idx_changed: ^ ^ + * boundaries: [1] [0] + * + * This means when X is accessed: + * 1) every node at boundary with size_idx < X should increase its size_idx + * and the boundary pointer shall move to its next. + * 2) X's size_idx should be set to 0. + * 3) if X itself is a boundary, set that boundary to X's next (sucessor). */ + uint32_t size_idx; if (s) { // No new insertion size_idx = *h; - if (size_idx < num_ticks && size_boundaries[size_idx] == h.node) - size_boundaries[size_idx] = s.node; + if (size_idx < num_ticks - 1 && boundaries[size_idx] == h.node) + boundaries[size_idx] = s.node; } else { - assert(lru_size <= max_size); - if (lru_size < max_size) ++lru_size; - if (lru_size <= min_size) - size_idx = 0; - else - size_idx = (lru_size - min_size + tick - 1) / tick; - if (size_idx < num_ticks && lru_size - min_size == size_idx * tick) - size_boundaries[size_idx] = cache.lru_.next; + // New insertion happened may because + // 1) even max_size cannot cache the block + // 2) this block has never been accessed before + // For simplicity, both cases are handled uniformly by treating it as a miss + assert(cache.size() <= max_size); + size_idx = cache.size() > min_size + ? (cache.size() - min_size + tick - 1) / tick + : 0; + if (size_idx < num_ticks - 1 && cache.size() == size_idx * tick + min_size) + boundaries[size_idx] = cache.lru_.next; } for (uint32_t i = 0; i < size_idx; ++i) { - auto& b = size_boundaries[i]; + auto& b = boundaries[i]; if (!b) continue; b->value++; b = b->next; } + *h = 0; + switch (mode) { case AccessMode::DEFAULT: if (s) { @@ -205,8 +231,6 @@ inline void GhostCache::access_impl(uint32_t block_id, uint32_t hash, case AccessMode::NOOP: break; } - - *h = 0; } template @@ -214,18 +238,18 @@ inline std::ostream& GhostCache::print(std::ostream& os, int indent) const { os << "GhostCache (tick=" << tick << ", min=" << min_size << ", max=" << max_size << ", num_ticks=" << num_ticks - << ", lru_size=" << lru_size << ") {\n"; + << ", size=" << cache.size() << ") {\n"; for (int i = 0; i < indent + 1; ++i) os << '\t'; os << "Boundaries: ["; - if (size_boundaries[0]) - os << size_boundaries[0]->key; + if (boundaries[0]) + os << boundaries[0]->key; else os << "(null)"; - for (uint32_t i = 1; i < num_ticks; ++i) { + for (uint32_t i = 1; i < boundaries.size(); ++i) { os << ", "; - if (size_boundaries[i]) - os << size_boundaries[i]->key; + if (boundaries[i]) + os << boundaries[i]->key; else os << "(null)"; } diff --git a/include/gcache/stat.h b/include/gcache/stat.h index a409bf4..07f907d 100644 --- a/include/gcache/stat.h +++ b/include/gcache/stat.h @@ -39,12 +39,15 @@ struct CacheStat { std::ostream& print(std::ostream& os, int width = 0) const { uint64_t acc_cnt = hit_cnt + miss_cnt; if (acc_cnt == 0) - return os << " NAN (" << std::setw(width) << std::fixed << hit_cnt + return os << " NAN (" << std::setw(width) << std::fixed << hit_cnt << '/' << std::setw(width) << std::fixed << acc_cnt << ')'; - return os << std::setw(6) << std::fixed << std::setprecision(2) - << get_hit_rate() * 100 << "% (" << std::setw(width) << std::fixed - << hit_cnt << '/' << std::setw(width) << std::fixed << acc_cnt - << ')'; + if (miss_cnt == 0) + os << " 100%"; + else + os << std::setw(4) << std::fixed << std::setprecision(1) + << get_hit_rate() * 100 << '%'; + return os << " (" << std::setw(width) << std::fixed << hit_cnt << '/' + << std::setw(width) << std::fixed << acc_cnt << ')'; } // print for debugging diff --git a/tests/test_ghost.cpp b/tests/test_ghost.cpp index 257d67b..43e4b18 100644 --- a/tests/test_ghost.cpp +++ b/tests/test_ghost.cpp @@ -16,93 +16,146 @@ constexpr const uint32_t large_bench_size = 2 * 1024 * 1024; // 8 GB cache constexpr const uint32_t sample_shift = 5; void test1() { - GhostCache<> ghost_cache(1, 3, 6); std::cout << "=== Test 1 ===\n"; + GhostCache<> ghost_cache(1, 3, 6); ghost_cache.access(0); ghost_cache.access(1); ghost_cache.access(2); ghost_cache.access(3); - std::cout << "Expect: Boundaries: [1, 0, (null), (null)]; " + std::cout << "Ops: Access [0, 1, 2, 3]" << std::endl; + std::cout << "Expect: Boundaries: [1, 0, (null)]; " "Stat: [0/4, 0/4, 0/4, 0/4]\n"; - std::cout << ghost_cache; + std::cout << ghost_cache << std::endl; ghost_cache.access(4); ghost_cache.access(5); - std::cout << "Expect: Boundaries: [3, 2, 1, 0]; Stat: [0/6, 0/6, 0/6, 0/6]\n"; - std::cout << ghost_cache; + std::cout << "Ops: Access [4, 5]" << std::endl; + std::cout << "Expect: Boundaries: [3, 2, 1]; Stat: [0/6, 0/6, 0/6, 0/6]\n"; + std::cout << ghost_cache << std::endl; ghost_cache.access(2); - std::cout << "Expect: Boundaries: [4, 3, 1, 0]; Stat: [0/7, 1/7, 1/7, 1/7]\n"; - std::cout << ghost_cache; + std::cout << "Ops: Access [2]" << std::endl; + std::cout << "Expect: Boundaries: [4, 3, 1]; Stat: [0/7, 1/7, 1/7, 1/7]\n"; + std::cout << ghost_cache << std::endl; ghost_cache.access(4); - std::cout << "Expect: Boundaries: [5, 3, 1, 0]; Stat: [1/8, 2/8, 2/8, 2/8]\n"; - std::cout << ghost_cache; - std::cout << std::flush; + std::cout << "Ops: Access [4]" << std::endl; + std::cout << "Expect: Boundaries: [5, 3, 1]; Stat: [1/8, 2/8, 2/8, 2/8]\n"; + std::cout << ghost_cache << std::endl; ghost_cache.access(2, AccessMode::AS_MISS); - std::cout << "Expect: Boundaries: [5, 3, 1, 0]; Stat: [1/9, 2/9, 2/9, 2/9]\n"; - std::cout << ghost_cache; - std::cout << std::flush; + std::cout << "Ops: Access [2:AS_MISS]" << std::endl; + std::cout << "Expect: Boundaries: [5, 3, 1]; Stat: [1/9, 2/9, 2/9, 2/9]\n"; + std::cout << ghost_cache << std::endl; ghost_cache.access(0, AccessMode::AS_HIT); + std::cout << "Ops: Access [0:AS_HIT]" << std::endl; std::cout - << "Expect: Boundaries: [4, 5, 3, 1]; Stat: [2/10, 3/10, 3/10, 3/10]\n"; - std::cout << ghost_cache; - std::cout << std::flush; + << "Expect: Boundaries: [4, 5, 3]; Stat: [2/10, 3/10, 3/10, 3/10]\n"; + std::cout << ghost_cache << std::endl; ghost_cache.access(7, AccessMode::NOOP); + std::cout << "Ops: Access [7:NOOP]" << std::endl; std::cout - << "Expect: Boundaries: [2, 4, 5, 3]; Stat: [2/10, 3/10, 3/10, 3/10]\n"; - std::cout << ghost_cache; - std::cout << std::flush; + << "Expect: Boundaries: [2, 4, 5]; Stat: [2/10, 3/10, 3/10, 3/10]\n"; + std::cout << ghost_cache << std::endl; } void test2() { - GhostCache<> ghost_cache(2, 2, 6); std::cout << "=== Test 2 ===\n"; + GhostCache<> ghost_cache(2, 2, 6); ghost_cache.access(0); ghost_cache.access(1); ghost_cache.access(2); ghost_cache.access(3); - std::cout << "Expect: Boundaries: [2, 0, (null)]; Stat: [0/4, 0/4, 0/4]\n"; - std::cout << ghost_cache; + std::cout << "Ops: Access [0, 1, 2, 3]" << std::endl; + std::cout << "Expect: Boundaries: [2, 0]; Stat: [0/4, 0/4, 0/4]\n"; + std::cout << ghost_cache << std::endl; ghost_cache.access(4); ghost_cache.access(5); - std::cout << "Expect: Boundaries: [4, 2, 0]; Stat: [0/6, 0/6, 0/6]\n"; - std::cout << ghost_cache; + std::cout << "Ops: Access [4, 5]" << std::endl; + std::cout << "Expect: Boundaries: [4, 2]; Stat: [0/6, 0/6, 0/6]\n"; + std::cout << ghost_cache << std::endl; ghost_cache.access(6); ghost_cache.access(7); - std::cout << "Expect: Boundaries: [6, 4, 2]; Stat: [0/8, 0/8, 0/8]\n"; - std::cout << ghost_cache; + std::cout << "Ops: Access [6, 7]" << std::endl; + std::cout << "Expect: Boundaries: [6, 4]; Stat: [0/8, 0/8, 0/8]\n"; + std::cout << ghost_cache << std::endl; ghost_cache.access(1); - std::cout << "Expect: Boundaries: [7, 5, 3]; Stat: [0/9, 0/9, 0/9]\n"; - std::cout << ghost_cache; + std::cout << "Ops: Access [1]" << std::endl; + std::cout << "Expect: Boundaries: [7, 5]; Stat: [0/9, 0/9, 0/9]\n"; + std::cout << ghost_cache << std::endl; ghost_cache.access(4); - std::cout << "Expect: Boundaries: [1, 6, 3]; Stat: [0/10, 0/10, 1/10]\n"; - std::cout << ghost_cache; - std::cout << std::flush; + std::cout << "Ops: Access [4]" << std::endl; + std::cout << "Expect: Boundaries: [1, 6]; Stat: [0/10, 0/10, 1/10]\n"; + std::cout << ghost_cache << std::endl; ghost_cache.access(8, AccessMode::NOOP); - std::cout << "Expect: Boundaries: [4, 7, 5]; Stat: [0/10, 0/10, 1/10]\n"; - std::cout << ghost_cache; - std::cout << std::flush; + std::cout << "Ops: Access [8:NOOP]" << std::endl; + std::cout << "Expect: Boundaries: [4, 7]; Stat: [0/10, 0/10, 1/10]\n"; + std::cout << ghost_cache << std::endl; ghost_cache.access(9, AccessMode::AS_HIT); - std::cout << "Expect: Boundaries: [8, 1, 6]; Stat: [1/11, 1/11, 2/11]\n"; - std::cout << ghost_cache; - std::cout << std::flush; + std::cout << "Ops: Access [9:AS_HIT]" << std::endl; + std::cout << "Expect: Boundaries: [8, 1]; Stat: [1/11, 1/11, 2/11]\n"; + std::cout << ghost_cache << std::endl; ghost_cache.access(1, AccessMode::AS_MISS); - std::cout << "Expect: Boundaries: [9, 4, 6]; Stat: [1/12, 1/12, 2/12]\n"; - std::cout << ghost_cache; - std::cout << std::flush; + std::cout << "Ops: Access [1:AS_MISS]" << std::endl; + std::cout << "Expect: Boundaries: [9, 4]; Stat: [1/12, 1/12, 2/12]\n"; + std::cout << ghost_cache << std::endl; +} + +// for checkpoint and recover +void test3() { + GhostCache<> ghost_cache(2, 2, 6); + std::cout << "=== Test 3 ===\n"; + + ghost_cache.access(0); + ghost_cache.access(1); + ghost_cache.access(2); + ghost_cache.access(3); + ghost_cache.access(4); + ghost_cache.access(5); + ghost_cache.access(6); + ghost_cache.access(7); + ghost_cache.access(1); + ghost_cache.access(4); + ghost_cache.access(8); + ghost_cache.access(9); + ghost_cache.access(1); + std::cout << "Ops: Access [0, 1, 2, 3, 4, 5, 6, 7, 1, 4, 8, 9, 1]" + << std::endl; + + std::vector ckpt; + ghost_cache.for_each_lru([&ckpt](uint32_t key) { ckpt.emplace_back(key); }); + + GhostCache<> ghost_cache2(3, 2, 11); + for (auto key : ckpt) ghost_cache2.access(key, AccessMode::NOOP); + + std::cout << "Recover from checkpoint" << std::endl; + std::cout + << "Expect: LRU: [6, 7, 4, 8, 9, 1]; Boundaries: [9, 7, (null), (null)]; " + "Stat: [0/0, 0/0, 0/0, 0/0]\n"; + std::cout << ghost_cache2; + + std::cout << "Ops: Access [2, 4, 3, 0]" << std::endl; + ghost_cache2.access(2); + ghost_cache2.access(4); + ghost_cache2.access(3); + ghost_cache2.access(0); + std::cout << "Expect: LRU: [6, 7, 8, 9, 1, 2, 4, 3, 0]; Boundaries: [3, 1, " + "7, (null)]; " + "Stat: [0/4, 1/4, 1/4, 1/4]\n"; + std::cout << ghost_cache2; + + std::cout << std::endl; } void bench1() { @@ -140,7 +193,7 @@ void bench1() { std::cout << "Hit: " << (ts2 - ts1) / bench_size << " cycles/op\n"; std::cout << "Miss: " << (ts3 - ts2) / bench_size << " cycles/op\n"; std::cout << "Hash: " << (ts4 - ts3) / bench_size << " cycles/op\n"; - std::cout << std::flush; + std::cout << std::endl; } void bench2() { @@ -165,7 +218,7 @@ void bench2() { std::cout << "Fill: " << (ts1 - ts0) / bench_size << " cycles/op\n"; std::cout << "Hit: " << (ts2 - ts1) / bench_size << " cycles/op\n"; std::cout << "Miss: " << (ts3 - ts2) / bench_size << " cycles/op\n"; - std::cout << std::flush; + std::cout << std::endl; } void bench3() { @@ -220,12 +273,12 @@ void bench3() { } std::cout << "================================================================\n"; - std::cout << std::flush; + std::cout << std::endl; } void bench4() { GhostCache<> ghost_cache(large_bench_size / 32, large_bench_size / 32, - large_bench_size); + large_bench_size); SampledGhostCache sampled_ghost_cache( large_bench_size / 32, large_bench_size / 32, large_bench_size); @@ -276,12 +329,12 @@ void bench4() { } std::cout << "================================================================\n"; - std::cout << std::flush; + std::cout << std::endl; } void bench5() { GhostCache<> ghost_cache(large_bench_size / 32, large_bench_size / 32, - large_bench_size); + large_bench_size); SampledGhostCache sampled_ghost_cache( large_bench_size / 32, large_bench_size / 32, large_bench_size); @@ -324,12 +377,13 @@ void bench5() { } std::cout << "================================================================\n"; - std::cout << std::flush; + std::cout << std::endl; } int main() { test1(); test2(); + test3(); // test checkpoint and recover bench1(); // ghost cache w/o sampling bench2(); // ghost cache w/ sampling bench3(); // hit rate comparsion diff --git a/tests/test_lru.cpp b/tests/test_lru.cpp index 4813ade..fdce40e 100644 --- a/tests/test_lru.cpp +++ b/tests/test_lru.cpp @@ -162,7 +162,6 @@ void test() { std::cout << "{ "; cache.for_each([](LRUCache::Handle_t h) { std::cout << h.get_key() << ": " << *h << ", "; - assert(key == value); }); std::cout << "}" << std::endl;