├── .gitignore
├── LICENSE.md
├── README.md
├── build.zig
├── build.zig.zon
├── lfu
├── README.md
└── titylfu.zig
├── lru
└── lru.zig
├── s3fifo
├── README.md
├── ghost.zig
└── s3fifo.zig
├── sieve
├── README.md
└── sieve.zig
└── src
└── main.zig
/.gitignore:
--------------------------------------------------------------------------------
1 | /zig-cache
2 | /zig-out
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Jeevananthan
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ziglang-caches
2 |
3 | This is a modern cache implementation, inspired by the following papers, provides high efficiency.
4 |
5 | - SIEVE | [SIEVE is Simpler than LRU: an Efficient Turn-Key Eviction Algorithm for Web Caches (NSDI'24)](https://junchengyang.com/publication/nsdi24-SIEVE.pdf)
6 | - S3-FIFO | [FIFO queues are all you need for cache eviction (SOSP'23)](https://dl.acm.org/doi/10.1145/3600006.3613147)
7 | - W-TinyLFU | [TinyLFU: A Highly Efficient Cache Admission Policy](https://arxiv.org/abs/1512.00727)
8 |
9 | This offers state-of-the-art efficiency and scalability compared to other LRU-based cache algorithms.
10 |
11 | ## Basic usage
12 | > [!LRU_Cache]
13 | > Least recents used cache eviction policy for cache your data in-memory for fast access.
14 |
15 |
16 | ```zig
17 | const std = @import("std");
18 | const lru = @import("lru");
19 |
20 | const cache = lru.LruCache(.locking, u8, []const u8);
21 |
22 | pub fn main() !void {
23 |
24 | // Create a cache backed by DRAM
25 | var lrucache = try cache.init(std.heap.page_allocator, 4);
26 | defer lrucache.deinit();
27 |
28 | // Add an object to the cache
29 | try lrucache.insert(1, "one");
30 | try lrucache.insert(2, "two");
31 | try lrucache.insert(3, "three");
32 | try lrucache.insert(4, "four");
33 |
34 | // Most recently used cache
35 | std.debug.print("mru: {s} \n", .{lrucache.mru().?.value});
36 |
37 | // least recently used cache
38 | std.debug.print("lru: {s} \n", .{lrucache.lru().?.value});
39 |
40 | // remove from cache
41 | _ = lrucache.remove(1);
42 |
43 | // Check if an object is in the cache O/P: false
44 | std.debug.print("key: 1 exists: {} \n", .{lrucache.contains(1)});
45 | }
46 | ```
47 |
48 | ### :rocket: Usage
49 |
50 | 1. Add `ziglang-caches` as a dependency in your `build.zig.zon`.
51 |
52 |
53 |
54 | build.zig.zon
example
55 |
56 | ```zig
57 | .{
58 | .name = "",
59 | .version = "",
60 | .dependencies = .{
61 | .caches = .{
62 | .url = "https://github.com/jeevananthan-23/ziglang-caches/archive/.tar.gz",
63 | .hash = "",
64 | },
65 | },
66 | }
67 | ```
68 |
69 | Set `` to `12200000000000000000000000000000000000000000000000000000000000000000`, and Zig will provide the correct found value in an error message.
70 |
71 |
72 |
73 | 2. Add `lrucache` as a module in your `build.zig`.
74 |
75 |
76 |
77 | build.zig
example
78 |
79 | ```zig
80 | const lrucache = b.dependency("caches", .{});
81 | exe.addModule("lrucache", lrucache.module("lrucache"));
82 | ```
83 |
84 |
85 |
--------------------------------------------------------------------------------
/build.zig:
--------------------------------------------------------------------------------
1 | const std = @import("std");
2 |
3 | pub fn build(b: *std.Build) void {
4 | const target = b.standardTargetOptions(.{});
5 | const optimize = b.standardOptimizeOption(.{});
6 |
7 | // Main modules for projects to use.
8 | const lru_module = b.addModule("lrucache", .{ .root_source_file = .{ .path = "lru/lru.zig" } });
9 |
10 | _ = b.addModule("s3fifocache", .{ .root_source_file = .{ .path = "s3fifo/s3fifo.zig" } });
11 |
12 | // Library
13 | const lib_step = b.step("lib", "Install library");
14 |
15 | const liblru = b.addStaticLibrary(.{
16 | .name = "lrucache",
17 | .root_source_file = .{ .path = "lru/lru.zig" },
18 | .target = target,
19 | .optimize = optimize,
20 | .version = .{ .major = 0, .minor = 1, .patch = 0 },
21 | });
22 |
23 | const libs3fifo = b.addStaticLibrary(.{
24 | .name = "s3fifocache",
25 | .root_source_file = .{ .path = "s3fifo/s3fifo.zig" },
26 | .target = target,
27 | .optimize = optimize,
28 | .version = .{ .major = 0, .minor = 1, .patch = 0 },
29 | });
30 |
31 | const liblru_install = b.addInstallArtifact(liblru, .{});
32 | const libs3fifo_instal = b.addInstallArtifact(libs3fifo, .{});
33 |
34 | lib_step.dependOn(&liblru_install.step);
35 | lib_step.dependOn(&libs3fifo_instal.step);
36 | b.default_step.dependOn(lib_step);
37 |
38 | // Docs
39 | const docs_step = b.step("docs", "Emit docs");
40 |
41 | const docs_install = b.addInstallDirectory(.{
42 | .source_dir = libs3fifo.getEmittedDocs(),
43 | .install_dir = .prefix,
44 | .install_subdir = "docs",
45 | });
46 |
47 | docs_step.dependOn(&docs_install.step);
48 | b.default_step.dependOn(docs_step);
49 |
50 | // Sample cache in Main
51 | const run_step = b.step("run", "Run the app");
52 |
53 | const exe = b.addExecutable(.{
54 | .name = "ziglang-caches",
55 | .root_source_file = .{ .path = "src/main.zig" },
56 | .target = target,
57 | .optimize = optimize,
58 | });
59 |
60 | exe.root_module.addImport("lru", lru_module);
61 |
62 | b.installArtifact(exe);
63 |
64 | const run_cmd = b.addRunArtifact(exe);
65 | run_cmd.step.dependOn(b.getInstallStep());
66 |
67 | run_step.dependOn(&run_cmd.step);
68 |
69 | // Tests
70 | const test_step = b.step("test", "Run unit tests");
71 |
72 | const liblru_unit_tests = b.addTest(.{
73 | .root_source_file = .{ .path = "lru/lru.zig" },
74 | .target = target,
75 | .optimize = optimize,
76 | });
77 |
78 | const run_liblru_unit_tests = b.addRunArtifact(liblru_unit_tests);
79 |
80 | const libs3fifo_unit_tests = b.addTest(.{
81 | .root_source_file = .{ .path = "s3fifo/s3fifo.zig" },
82 | .target = target,
83 | .optimize = optimize,
84 | });
85 |
86 | const run_libs3fifo_unit_tests = b.addRunArtifact(libs3fifo_unit_tests);
87 |
88 | test_step.dependOn(&run_liblru_unit_tests.step);
89 | test_step.dependOn(&run_libs3fifo_unit_tests.step);
90 |
91 | // Lints
92 | const lints_step = b.step("lint", "Run lints");
93 |
94 | const lints = b.addFmt(.{
95 | .paths = &.{ "src", "build.zig" },
96 | .check = true,
97 | });
98 |
99 | lints_step.dependOn(&lints.step);
100 | b.default_step.dependOn(lints_step);
101 | }
102 |
--------------------------------------------------------------------------------
/build.zig.zon:
--------------------------------------------------------------------------------
1 | .{
2 | .name = "ziglanf-caches",
3 | .version = "0.1.0",
4 | .minimum_zig_version = "0.12.0-dev.2334+aef1da163",
5 | .paths = .{
6 | ".",
7 | },
8 | }
9 |
--------------------------------------------------------------------------------
/lfu/README.md:
--------------------------------------------------------------------------------
1 | Window TinyLfu
2 | W-TinyLfu uses a small admission LRU that evicts to a large Segmented LRU if accepted by the TinyLfu admission policy. TinyLfu relies on a frequency sketch to probabilistically estimate the historic usage of an entry. The window allows the policy to have a high hit rate when entries exhibit recency bursts which would otherwise be rejected. The size of the window vs main space is adaptively determined using a hill climbing optimization. This configuration enables the cache to estimate the frequency and recency of an entry with low overhead.
3 |
4 | This implementation uses a 4-bit CountMinSketch, growing at 8 bytes per cache entry to be accurate. Unlike ARC and LIRS, this policy does not retain evicted keys.
5 |
6 |
7 | - [TinyLFU: A Highly Efficient Cache Admission Policy](https://arxiv.org/abs/1512.00727)
--------------------------------------------------------------------------------
/lfu/titylfu.zig:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Jeevananthan-23/ziglang-caches/5e4d837a4652b0e905e8e88eebe8dcf0b58832ca/lfu/titylfu.zig
--------------------------------------------------------------------------------
/lru/lru.zig:
--------------------------------------------------------------------------------
1 | const std = @import("std");
2 | const Allocator = std.mem.Allocator;
3 | const TailQueue = std.TailQueue;
4 | const testing = std.testing;
5 | const assert = std.debug.assert;
6 | const Mutex = std.Thread.RwLock;
7 | const Atomic = std.atomic;
8 |
9 | pub const Kind = enum {
10 | locking,
11 | non_locking,
12 | };
13 |
14 | /// Concurrent LRUCache using RWLock
15 | pub fn LruCache(comptime kind: Kind, comptime K: type, comptime V: type) type {
16 | return struct {
17 | mux: if (kind == .locking) Mutex else void,
18 | allocator: Allocator,
19 | hashmap: if (K == []const u8) std.StringArrayHashMap(*Node) else std.AutoArrayHashMap(K, *Node),
20 | dbl_link_list: TailQueue(LruEntry),
21 | max_items: usize,
22 | len: usize,
23 |
24 | const Self = @This();
25 |
26 | pub const LruEntry = struct {
27 | key: K,
28 | value: V,
29 |
30 | const Self = @This();
31 |
32 | pub fn init(key: K, val: V) LruEntry {
33 | return LruEntry{
34 | .key = key,
35 | .value = val,
36 | };
37 | }
38 | };
39 |
40 | const Node = TailQueue(LruEntry).Node;
41 |
42 | fn initNode(self: *Self, key: K, val: V) error{OutOfMemory}!*Node {
43 | self.len += 1;
44 | const node = try self.allocator.create(Node);
45 | node.* = .{ .data = LruEntry.init(key, val) };
46 | return node;
47 | }
48 |
49 | fn deinitNode(self: *Self, node: *Node) void {
50 | self.len -= 1;
51 | self.allocator.destroy(node);
52 | }
53 |
54 | pub fn init(allocator: Allocator, max_items: usize) error{OutOfMemory}!Self {
55 | const hashmap = if (K == []const u8) std.StringArrayHashMap(*Node).init(allocator) else std.AutoArrayHashMap(K, *Node).init(allocator);
56 | var self = Self{
57 | .allocator = allocator,
58 | .hashmap = hashmap,
59 | .dbl_link_list = TailQueue(LruEntry){},
60 | .max_items = max_items,
61 | .mux = if (kind == .locking) Mutex{} else undefined,
62 | .len = 0,
63 | };
64 |
65 | // pre allocate enough capacity for max items since we will use
66 | // assumed capacity and non-clobber methods
67 | try self.hashmap.ensureTotalCapacity(self.max_items);
68 |
69 | return self;
70 | }
71 |
72 | pub fn deinit(self: *Self) void {
73 | while (self.dbl_link_list.pop()) |node| {
74 | self.deinitNode(node);
75 | }
76 | std.debug.assert(self.len == 0); // no leaks
77 | self.hashmap.deinit();
78 | }
79 |
80 | /// Recycles an old node if LruCache capacity is full. If replaced, first element of tuple is replaced
81 | /// Entry (otherwise null) and second element of tuple is inserted Entry.
82 | fn internal_recycle_or_create_node(self: *Self, key: K, value: V) error{OutOfMemory}!struct { ?LruEntry, LruEntry } {
83 | if (self.dbl_link_list.len == self.max_items) {
84 | const recycled_node = self.dbl_link_list.popFirst().?;
85 | assert(self.hashmap.swapRemove(recycled_node.data.key));
86 | // after swap, this node is thrown away
87 | var node_to_swap: Node = .{
88 | .data = LruEntry.init(key, value),
89 | .next = null,
90 | .prev = null,
91 | };
92 | std.mem.swap(Node, recycled_node, &node_to_swap);
93 | self.dbl_link_list.append(recycled_node);
94 | self.hashmap.putAssumeCapacityNoClobber(key, recycled_node);
95 | return .{ node_to_swap.data, recycled_node.data };
96 | }
97 |
98 | // key not exist, alloc a new node
99 | const node = try self.initNode(key, value);
100 | self.hashmap.putAssumeCapacityNoClobber(key, node);
101 | self.dbl_link_list.append(node);
102 | return .{ null, node.data };
103 | }
104 |
105 | fn internal_insert(self: *Self, key: K, value: V) LruEntry {
106 | // if key exists, we update it
107 | if (self.hashmap.get(key)) |existing_node| {
108 | existing_node.data.value = value;
109 | self.internal_reorder(existing_node);
110 | return existing_node.data;
111 | }
112 |
113 | const replaced_and_created_node = self.internal_recycle_or_create_node(key, value) catch |e| {
114 | std.debug.print("replace_or_create_node returned error: {any}", .{e});
115 | @panic("could not recycle_or_create_node");
116 | };
117 | const new_lru_entry = replaced_and_created_node[1];
118 | return new_lru_entry;
119 | }
120 |
121 | /// Inserts key/value if key doesn't exist, updates only value if it does.
122 | /// In any case, it will affect cache ordering.
123 | pub fn insert(self: *Self, key: K, value: V) error{OutOfMemory}!void {
124 | if (kind == .locking) {
125 | self.mux.lock();
126 | defer self.mux.unlock();
127 | }
128 |
129 | _ = self.internal_insert(key, value);
130 | return;
131 | }
132 |
133 | /// Whether or not contains key.
134 | /// NOTE: doesn't affect cache ordering.
135 | pub fn contains(self: *Self, key: K) bool {
136 | if (kind == .locking) {
137 | self.mux.lockShared();
138 | defer self.mux.unlockShared();
139 | }
140 |
141 | return self.hashmap.contains(key);
142 | }
143 |
144 | /// Most recently used entry
145 | pub fn mru(self: *Self) ?LruEntry {
146 | if (kind == .locking) {
147 | self.mux.lockShared();
148 | defer self.mux.unlockShared();
149 | }
150 |
151 | if (self.dbl_link_list.last) |node| {
152 | return node.data;
153 | }
154 | return null;
155 | }
156 |
157 | /// Least recently used entry
158 | pub fn lru(self: *Self) ?LruEntry {
159 | if (kind == .locking) {
160 | self.mux.lockShared();
161 | defer self.mux.unlockShared();
162 | }
163 |
164 | if (self.dbl_link_list.first) |node| {
165 | return node.data;
166 | }
167 | return null;
168 | }
169 |
170 | // reorder Node to the top
171 | fn internal_reorder(self: *Self, node: *Node) void {
172 | self.dbl_link_list.remove(node);
173 | self.dbl_link_list.append(node);
174 | }
175 |
176 | /// Gets value associated with key if exists
177 | pub fn get(self: *Self, key: K) ?V {
178 | if (kind == .locking) {
179 | self.mux.lockShared();
180 | defer self.mux.unlockShared();
181 | }
182 |
183 | if (self.hashmap.get(key)) |node| {
184 | self.dbl_link_list.remove(node);
185 | self.dbl_link_list.append(node);
186 | return node.data.value;
187 | }
188 | return null;
189 | }
190 |
191 | pub fn pop(self: *Self, k: K) ?V {
192 | if (kind == .locking) {
193 | self.mux.lock();
194 | defer self.mux.unlock();
195 | }
196 |
197 | if (self.hashmap.fetchSwapRemove(k)) |kv| {
198 | const node = kv.value;
199 | self.dbl_link_list.remove(node);
200 | defer self.deinitNode(node);
201 | return node.data.value;
202 | }
203 | return null;
204 | }
205 |
206 | pub fn peek(self: *Self, key: K) ?V {
207 | if (kind == .locking) {
208 | self.mux.lockShared();
209 | defer self.mux.unlockShared();
210 | }
211 |
212 | if (self.hashmap.get(key)) |node| {
213 | return node.data.value;
214 | }
215 |
216 | return null;
217 | }
218 |
219 | /// Puts a key-value pair into cache. If the key already exists in the cache, then it updates
220 | /// the key's value and returns the old value. Otherwise, `null` is returned.
221 | pub fn put(self: *Self, key: K, value: V) ?V {
222 | if (kind == .locking) {
223 | self.mux.lock();
224 | defer self.mux.unlock();
225 | }
226 |
227 | if (self.hashmap.getEntry(key)) |existing_entry| {
228 | var existing_node: *Node = existing_entry.value_ptr.*;
229 | const old_value = existing_node.data.value;
230 | existing_node.data.value = value;
231 | self.internal_reorder(existing_node);
232 | return old_value;
233 | }
234 |
235 | _ = self.internal_insert(key, value);
236 | return null;
237 | }
238 |
239 | /// Removes key from cache. Returns true if found, false if not.
240 | pub fn remove(self: *Self, key: K) bool {
241 | if (kind == .locking) {
242 | self.mux.lock();
243 | defer self.mux.unlock();
244 | }
245 |
246 | if (self.hashmap.fetchSwapRemove(key)) |kv| {
247 | const node = kv.value;
248 | self.dbl_link_list.remove(node);
249 | self.deinitNode(node);
250 | return true;
251 | }
252 | return false;
253 | }
254 | };
255 | }
256 |
257 | test "common.lru: LruCache state is correct" {
258 | var cache = try LruCache(.locking, u64, []const u8).init(testing.allocator, 4);
259 | defer cache.deinit();
260 |
261 | try cache.insert(1, "one");
262 | try cache.insert(2, "two");
263 | try cache.insert(3, "three");
264 | try cache.insert(4, "four");
265 | try testing.expectEqual(@as(usize, 4), cache.dbl_link_list.len);
266 | try testing.expectEqual(@as(usize, 4), cache.hashmap.keys().len);
267 | try testing.expectEqual(@as(usize, 4), cache.len);
268 |
269 | const val = cache.get(2);
270 | try testing.expectEqual(val.?, "two");
271 | try testing.expectEqual(cache.mru().?.value, "two");
272 | try testing.expectEqual(cache.lru().?.value, "one");
273 |
274 | try cache.insert(5, "five");
275 | try testing.expectEqual(cache.mru().?.value, "five");
276 | try testing.expectEqual(cache.lru().?.value, "three");
277 | try testing.expectEqual(@as(usize, 4), cache.dbl_link_list.len);
278 | try testing.expectEqual(@as(usize, 4), cache.hashmap.keys().len);
279 | try testing.expectEqual(@as(usize, 4), cache.len);
280 |
281 | try testing.expect(!cache.contains(1));
282 | try testing.expect(cache.contains(4));
283 |
284 | try testing.expect(cache.remove(5));
285 | try testing.expectEqualStrings("two", cache.mru().?.value);
286 | try testing.expectEqual(cache.len, 3);
287 | }
288 |
289 | test "common.lru: put works as expected" {
290 | var cache = try LruCache(.non_locking, []const u8, usize).init(testing.allocator, 4);
291 | defer cache.deinit();
292 |
293 | try cache.insert("a", 1);
294 |
295 | const old = cache.put("a", 2);
296 |
297 | try testing.expectEqual(@as(usize, 1), old.?);
298 | try testing.expectEqual(@as(usize, 2), cache.get("a").?);
299 |
300 | const possible_old = cache.put("b", 3);
301 | try testing.expectEqual(possible_old, null);
302 | try testing.expectEqual(@as(usize, 3), cache.get("b").?);
303 | }
304 |
--------------------------------------------------------------------------------
/s3fifo/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | An illustration of S3-FIFO.
4 |
5 |
6 | S3-FIFO uses three FIFO queues: a small FIFO queue (S), a main FIFO queue (M), and a ghost FIFO queue (G). We choose S to use 10% of the cache space based on experiments with 10 traces and find that 10% generalizes well. M then uses 90% of the cache space. The ghost queue G stores the same number of ghost entries (no data) as M.
7 |
8 | Cache read: S3-FIFO uses two bits per object to track object access status similar to a capped counter with frequency up to 31. Cache hits in S3-FIFO increment the counter by one atomically. Note that most requests for popular objects require no update.
9 |
10 | Cache write: New objects are inserted into S if not in G. Otherwise, it is inserted into M. When S is full, the object at the tail is either moved to M if it is accessed more than once or G if not. And its access bits are cleared during the move.
11 | When G is full, it evicts objects in FIFO order. M uses an algorithm similar to FIFO-Reinsertion but tracks access information using two bits. Objects that have been accessed at least once are reinserted with one bit set to 0 (similar to decreasing frequency by 1).
12 |
13 | Implementation¶
14 | Although S3-FIFO has three FIFO queues, it can also be implemented with one or two FIFO queue(s). Because objects evicted from S may enter M, they can be implemented using one queue with a pointer pointed at the 10% mark. However, combining S and M reduces scalability because removing objects from the middle of the queue requires locking.
15 |
16 | The ghost FIFO queue G can be implemented as part of the indexing structure. For example, we can store the fingerprint and eviction time of ghost entries in a bucket-based hash table. The fingerprint stores a hash of the object using 4 bytes, and the eviction time is a timestamp measured in the number of objects inserted into G. We can find out whether an object is still in the queue by calculating the difference between current time and insertion time since it is a FIFO queue. The ghost entries stay in the hash table until they are no longer in the ghost queue. When an entry is evicted from the ghost queue, it is not immediately removed from the hash table. Instead, the hash table entry is removed during hash collision --- when the slot is needed to store other entries.
17 |
18 | WIP plaining
19 |
20 | A In-memory cache implementation with TinyLFU as the admission policy and [S3-FIFO](https://s3fifo.com/) as the eviction policy.
21 |
22 | TinyUFO improves cache hit ratio noticeably compared to LRU.
23 |
24 | TinyUFO is lock-free. It is very fast in the systems with a lot concurrent reads and/or writes
25 |
--------------------------------------------------------------------------------
/s3fifo/ghost.zig:
--------------------------------------------------------------------------------
1 | const std = @import("std");
2 |
3 | // ghost represents the `ghost` structure in the S3FIFO algorithm.
4 |
--------------------------------------------------------------------------------
/s3fifo/s3fifo.zig:
--------------------------------------------------------------------------------
1 | //! A In-memory cache implementation with S3FIFO [S3-FIFO](https://s3fifo.com/) as the eviction policy.
2 | //!
3 | //! S3FIFO improves cache hit ratio noticeably compared to LRU.
4 | //!
5 | //! S3FIFO is using RWLock. It is very fast in the systems with a lot concurrent reads and/or writes
6 |
7 | const std = @import("std");
8 | const AtomicU8 = std.atomic.Value(u8);
9 | const Allocator = std.mem.Allocator;
10 | const TailQueue = std.TailQueue;
11 | const Mutex = std.Thread.RwLock;
12 | const testing = std.testing;
13 | const assert = std.debug.assert;
14 |
15 | /// Maximum frequency limit for an entry in the cache.
16 | const maxfreq: u8 = 3;
17 |
18 | pub const Kind = enum {
19 | locking,
20 | non_locking,
21 | };
22 |
23 | /// Simple implementation of "S3-FIFO" from "FIFO Queues are ALL You Need for Cache Eviction" by
24 | /// Juncheng Yang, et al: https://jasony.me/publication/sosp23-s3fifo.pdf
25 | pub fn S3fifo(comptime kind: Kind, K: type, comptime V: type) type {
26 | return struct {
27 | mux: if (kind == .locking) Mutex else void,
28 | allocator: Allocator,
29 | /// Small queue for entries with low frequency.
30 | small: TailQueue(Entry),
31 | /// Main queue for entries with high frequency.
32 | main: TailQueue(Entry),
33 | /// Ghost queue for evicted entries.
34 | ghost: TailQueue(K),
35 | /// Map of all entries for quick access.
36 | entries: if (K == []const u8) std.StringArrayHashMap(*Node) else std.AutoArrayHashMap(K, *Node),
37 | max_cache_size: usize,
38 | main_size: usize,
39 | len: usize,
40 |
41 | const Self = @This();
42 |
43 | /// Represents an entry in the cache.
44 | pub const Entry = struct {
45 | key: K,
46 | value: V,
47 | /// Frequency of access of this entry.
48 | feq: AtomicU8,
49 |
50 | const Self = @This();
51 |
52 | /// Creates a new entry with the given key and value.
53 | pub fn init(key: K, val: V) Entry {
54 | return Entry{
55 | .key = key,
56 | .value = val,
57 | .feq = AtomicU8.init(0),
58 | };
59 | }
60 | };
61 |
62 | const Node = TailQueue(Entry).Node;
63 |
64 | const gNode = TailQueue(K).Node;
65 |
66 | fn initNode(self: *Self, key: K, val: V) error{OutOfMemory}!*Node {
67 | self.len += 1;
68 |
69 | const node = try self.allocator.create(Node);
70 | node.* = .{ .data = Entry.init(key, val) };
71 | return node;
72 | }
73 |
74 | fn deinitNode(self: *Self, node: *Node) void {
75 | self.len -= 1;
76 | self.allocator.destroy(node);
77 | }
78 |
79 | /// Creates a new cache with the given maximum size.
80 | pub fn init(allocator: Allocator, max_cache_size: usize) Self {
81 | const max_small_size = max_cache_size / 10;
82 | const max_main_size = max_cache_size - max_small_size;
83 | const hashmap = if (K == []const u8) std.StringArrayHashMap(*Node).init(allocator) else std.AutoArrayHashMap(K, *Node).init(allocator);
84 | return Self{ .mux = if (kind == .locking) Mutex{} else undefined, .allocator = allocator, .small = TailQueue(Entry){}, .main = TailQueue(Entry){}, .ghost = TailQueue(K){}, .entries = hashmap, .max_cache_size = max_cache_size, .main_size = max_main_size, .len = 0 };
85 | }
86 |
87 | pub fn deinit(self: *Self) void {
88 | while (self.small.pop()) |node| {
89 | self.deinitNode(node);
90 | }
91 |
92 | while (self.ghost.pop()) |node| : (self.len -= 1) {
93 | self.allocator.destroy(node);
94 | }
95 |
96 | while (self.main.pop()) |node| {
97 | self.deinitNode(node);
98 | }
99 | std.debug.assert(self.len == 0); // no leaks
100 | self.entries.deinit();
101 | }
102 |
103 | /// Whether or not contains key.
104 | /// NOTE: doesn't affect cache ordering.
105 | pub fn contains(self: *Self, key: K) bool {
106 | if (kind == .locking) {
107 | self.mux.lockShared();
108 | defer self.mux.unlockShared();
109 | }
110 | return self.entries.contains(key);
111 | }
112 |
113 | /// Returns a reference to the value of the given key if it exists in the cache.
114 | pub fn get(self: *Self, key: K) ?V {
115 | if (kind == .locking) {
116 | self.mux.lockShared();
117 | defer self.mux.unlockShared();
118 | }
119 | if (self.entries.get(key)) |node| {
120 | const freq = @min(node.data.feq.load(.SeqCst) + 1, maxfreq);
121 | node.data.feq.store(freq, .SeqCst);
122 | return node.data.value;
123 | } else {
124 | return null;
125 | }
126 | }
127 |
128 | /// Inserts a new entry with the given key and value into the cache.
129 | pub fn insert(self: *Self, key: K, value: V) error{OutOfMemory}!void {
130 | if (kind == .locking) {
131 | self.mux.lock();
132 | defer self.mux.unlock();
133 | }
134 |
135 | try self.evict();
136 |
137 | if (self.entries.contains(key)) {
138 | const node = try self.initNode(key, value);
139 | self.main.append(node);
140 | } else {
141 | const node = try self.initNode(key, value);
142 | try self.entries.put(key, node);
143 | self.small.append(node);
144 | }
145 | }
146 |
147 | fn insert_m(self: *Self, tail: *Node) void {
148 | self.len += 1;
149 | self.main.prepend(tail);
150 | }
151 |
152 | fn insert_g(self: *Self, tail: *Node) !void {
153 | if (self.ghost.len >= self.main_size) {
154 | const key = self.ghost.popFirst().?;
155 | self.allocator.destroy(key);
156 | _ = self.entries.swapRemove(key.data);
157 | self.len -= 1;
158 | }
159 | const node = try self.allocator.create(gNode);
160 | node.* = .{ .data = tail.data.key };
161 | self.ghost.append(node);
162 | self.len += 1;
163 | }
164 |
165 | fn evict(self: *Self) !void {
166 | if (self.small.len + self.main.len >= self.max_cache_size) {
167 | if (self.main.len >= self.main_size or self.small.len == 0) {
168 | self.evict_m();
169 | } else {
170 | try self.evict_s();
171 | }
172 | }
173 | }
174 |
175 | fn evict_m(self: *Self) void {
176 | while (self.main.popFirst()) |tail| {
177 | const freq = tail.data.feq.load(.SeqCst);
178 | if (freq > 0) {
179 | tail.data.feq.store(freq - 1, .SeqCst);
180 | self.main.append(tail);
181 | } else {
182 | _ = self.entries.swapRemove(tail.data.key);
183 | self.deinitNode(tail);
184 | break;
185 | }
186 | }
187 | }
188 |
189 | fn evict_s(self: *Self) !void {
190 | while (self.small.popFirst()) |tail| {
191 | if (tail.data.feq.load(.SeqCst) > 1) {
192 | self.insert_m(tail);
193 | } else {
194 | try self.insert_g(tail);
195 | self.deinitNode(tail);
196 | break;
197 | }
198 | }
199 | }
200 | };
201 | }
202 |
203 | test "s3fifotest: base" {
204 | var gpa = std.heap.GeneralPurposeAllocator(.{}){};
205 | defer std.debug.print("\n GPA result: {}\n", .{gpa.deinit()});
206 | var logging_alloc = std.heap.loggingAllocator(gpa.allocator());
207 | const allocator = logging_alloc.allocator();
208 |
209 | var cache = S3fifo(.non_locking, u64, []const u8).init(allocator, 3);
210 | defer cache.deinit();
211 |
212 | try cache.insert(1, "one");
213 | try cache.insert(2, "two");
214 | const val = cache.get(1);
215 | try testing.expectEqual(val.?, "one");
216 | try cache.insert(3, "three");
217 | try cache.insert(4, "four");
218 | try cache.insert(5, "five");
219 | try cache.insert(4, "four");
220 | try testing.expect(cache.contains(1));
221 | }
222 |
223 | test "s3fifotest: push and read" {
224 | var cache = S3fifo(.locking, []const u8, []const u8).init(testing.allocator, 2);
225 | defer cache.deinit();
226 |
227 | try cache.insert("apple", "red");
228 | try cache.insert("banana", "yellow");
229 | const red = cache.get("apple");
230 | const yellow = cache.get("banana");
231 | try testing.expectEqual(red.?, "red");
232 | try testing.expectEqual(yellow.?, "yellow");
233 | }
234 |
--------------------------------------------------------------------------------
/sieve/README.md:
--------------------------------------------------------------------------------
1 | How does SIEVE work?
2 |
3 | 
--------------------------------------------------------------------------------
/sieve/sieve.zig:
--------------------------------------------------------------------------------
1 | //! A In-memory cache implementation with SIEVE [SIEVE](https://cachemon.github.io/SIEVE-website/) as the eviction policy.
2 | //!
3 | //! An Eviction Algorithm Simpler than LRU for Web Caches.
4 | //!
5 | //! SIEVE is using RWLock. It is very fast in the systems with a lot concurrent reads and/or writes
6 |
7 | const std = @import("std");
8 | const Allocator = std.mem.Allocator;
9 | const Mutex = std.Thread.RwLock;
10 |
11 | pub const Kind = enum {
12 | locking,
13 | non_locking,
14 | };
15 |
16 | /// Simple implementation of "SIEVE" from
17 | /// [SIEVE is Simpler than LRU: an Efficient Turn-Key Eviction Algorithm for Web Caches (NSDI'24)](https://junchengyang.com/publication/nsdi24-SIEVE.pdf)
18 | pub fn Sieve(comptime kind: Kind, comptime K: type, comptime V: type) type {
19 | return struct {
20 | const Self = @This();
21 | const HashMapUnmanaged = if (K == []const u8) std.StringHashMapUnmanaged(*Node) else std.AutoHashMapUnmanaged(K, *Node);
22 |
23 | /// Intrusive cache's node.
24 | pub const Node = struct {
25 | is_visited: bool = false,
26 | prev: ?*Node = null,
27 | next: ?*Node = null,
28 | value: V,
29 | key: K,
30 |
31 | /// Creates a new entry with the given key and value.
32 | pub fn init(key: K, val: V) Node {
33 | return Node{
34 | .key = key,
35 | .value = val,
36 | };
37 | }
38 | };
39 |
40 | mux: if (kind == .locking) Mutex else void,
41 | allocator: Allocator,
42 |
43 | map: HashMapUnmanaged = HashMapUnmanaged{},
44 | hand: ?*Node = null,
45 | head: ?*Node = null,
46 | tail: ?*Node = null,
47 |
48 | capacity: usize,
49 | len: usize,
50 |
51 | fn initNode(self: *Self, key: K, val: V) error{OutOfMemory}!*Node {
52 | self.len += 1;
53 |
54 | const node = try self.allocator.create(Node);
55 | node.* = .{ .key = key, .value = val };
56 | return node;
57 | }
58 |
59 | fn deinitNode(self: *Self, node: *Node) void {
60 | self.len -= 1;
61 | self.allocator.destroy(node);
62 | }
63 |
64 | /// Initialize cache with given capacity.
65 | pub fn init(allocator: std.mem.Allocator, max_size: u32) error{OutOfMemory}!Self {
66 | if (max_size == 0) @panic("Capacity must be greter than 0");
67 | var self = Self{
68 | .allocator = allocator,
69 | .mux = if (kind == .locking) Mutex{} else undefined,
70 | .capacity = max_size,
71 | .len = 0,
72 | };
73 |
74 | // pre allocate enough capacity for max items since we will use
75 | // assumed capacity and non-clobber methods
76 | try self.map.ensureTotalCapacity(allocator, max_size);
77 |
78 | return self;
79 | }
80 |
81 | /// Deinitialize cache.
82 | pub fn deinit(self: *Self, allocator: std.mem.Allocator) void {
83 | self.map.deinit(allocator);
84 | self.* = undefined;
85 | }
86 |
87 | pub fn reset(self: *Self) void {
88 | @memset(self.len, 0);
89 | @memset(self.capacity, 0);
90 | }
91 |
92 | /// Return the capacity of the cache.
93 | pub inline fn capacity(self: *Self) usize {
94 | self.capacity;
95 | }
96 |
97 | /// Return the number of cached values.
98 | pub inline fn len(self: *Self) usize {
99 | self.len;
100 | }
101 |
102 | /// Check if cache is empty.
103 | pub inline fn isEmpty(self: *Self) bool {
104 | return self.capacity() == 0;
105 | }
106 |
107 | /// Check if cache contains given key.
108 | pub fn contains(self: *Self, key: K) bool {
109 | return self.map.contains(key);
110 | }
111 |
112 | /// Get value associated with given key, otherwise return `null`.
113 | pub fn get(self: *Self, key: K) ?V {
114 | if (self.map.get(key)) |node| {
115 | node.is_visited = true;
116 | return node.value;
117 | }
118 | return null;
119 | }
120 |
121 | /// Put node pointer and return `true` if associated key is not already present.
122 | /// Otherwise, set node pointer, evicting old entry, and return `false`.
123 | pub fn set(self: *Self, key: K, value: V) error{OutOfMemory}!bool {
124 | var node = try self.initNode(key, value);
125 | if (self.map.getPtr(key)) |old_node| {
126 | old_node.* = node;
127 | return false;
128 | } else {
129 | if (self.len >= self.capacity) {
130 | self.evict();
131 | }
132 |
133 | node.next = self.head;
134 | if (self.head) |head| {
135 | head.prev = node;
136 | }
137 |
138 | self.head = node;
139 | if (self.tail == null) {
140 | self.tail = self.head;
141 | }
142 |
143 | self.map.putAssumeCapacityNoClobber(node.key, node);
144 | std.debug.assert(self.len < self.capacity);
145 | self.len += 1;
146 | return true;
147 | }
148 | }
149 |
150 | /// Remove key and return associated node pointer, otherwise return `null`.
151 | pub fn fetchRemove(self: *Self, key: K) ?*Node {
152 | const node = self.map.get(key) orelse return null;
153 | _ = self.map.remove(key);
154 | self.removeNode(node);
155 | std.debug.assert(self.len > 0);
156 | self.len -= 1;
157 | return node;
158 | }
159 |
160 | fn removeNode(self: *Self, node: *Node) void {
161 | if (node.prev) |prev| {
162 | prev.next = node.next;
163 | } else {
164 | self.head = node.next;
165 | }
166 |
167 | if (node.next) |next| {
168 | next.prev = node.prev;
169 | } else {
170 | self.tail = node.prev;
171 | }
172 | self.deinitNode(node);
173 | }
174 |
175 | fn evict(self: *Self) void {
176 | var node_opt = self.hand orelse self.tail;
177 | while (node_opt) |node| : (node_opt = node.prev orelse self.tail) {
178 | if (!node.is_visited) {
179 | break;
180 | }
181 | node.is_visited = false;
182 | }
183 | if (node_opt) |node| {
184 | self.hand = node.prev;
185 | _ = self.map.remove(node.key);
186 | self.removeNode(node);
187 | std.debug.assert(self.len > 0);
188 | self.len -= 1;
189 | }
190 | }
191 | };
192 | }
193 |
194 | test Sieve {
195 | const StringCache = Sieve(.non_locking, []const u8, []const u8);
196 |
197 | var cache = try StringCache.init(std.testing.allocator, 4);
198 | defer cache.deinit(std.testing.allocator);
199 |
200 | const foobar_node = StringCache.Node{ .key = "foo", .value = "bar" };
201 | const zigzag_node = StringCache.Node{ .key = "zig", .value = "zag" };
202 | const flipflop_node = StringCache.Node{ .key = "flip", .value = "flop" };
203 | const ticktock_node = StringCache.Node{ .key = "tick", .value = "tock" };
204 |
205 | try std.testing.expect(try cache.set(foobar_node.key, foobar_node.value));
206 | try std.testing.expect(try cache.set(zigzag_node.key, zigzag_node.value));
207 | try std.testing.expectEqual(2, cache.len);
208 | try std.testing.expect(try cache.set(flipflop_node.key, flipflop_node.value));
209 | try std.testing.expect(try cache.set(ticktock_node.key, ticktock_node.value));
210 | try std.testing.expectEqual(4, cache.capacity);
211 |
212 | try std.testing.expectEqualStrings("bar", cache.fetchRemove("foo").?.value);
213 | try std.testing.expectEqual(cache.get("foo"), null);
214 |
215 | try std.testing.expectEqualStrings("zag", cache.get("zig").?);
216 | try std.testing.expectEqualStrings("flop", cache.get("flip").?);
217 | try std.testing.expectEqualStrings("tock", cache.get("tick").?);
218 | }
219 |
--------------------------------------------------------------------------------
/src/main.zig:
--------------------------------------------------------------------------------
1 | const std = @import("std");
2 | const LRU = @import("lru").LruCache;
3 |
4 | const cache = LRU(.non_locking, u8, []const u8);
5 |
6 | pub fn main() !void {
7 |
8 | // Create a cache backed by DRAM
9 | var lrucache = try cache.init(std.heap.page_allocator, 4);
10 | defer lrucache.deinit();
11 |
12 | // Add an object to the cache
13 | try lrucache.insert(1, "one");
14 | try lrucache.insert(2, "two");
15 | try lrucache.insert(3, "three");
16 | try lrucache.insert(4, "four");
17 |
18 | // Most recently used cache
19 | std.debug.print("mru: {s} \n", .{lrucache.mru().?.value});
20 |
21 | // least recently used cache
22 | std.debug.print("lru: {s} \n", .{lrucache.lru().?.value});
23 |
24 | // remove from cache
25 | _ = lrucache.remove(1);
26 |
27 | // Check if an object is in the cache O/P: false
28 | std.debug.print("key: 1 exists: {} \n", .{lrucache.contains(1)});
29 | }
30 |
--------------------------------------------------------------------------------