Skip to content

Commit 9d524f4

Browse files
committed
Initial release of imcache: generic, sharded, zero-dependency in-memory cache for Go
0 parents  commit 9d524f4

14 files changed

Lines changed: 2528 additions & 0 deletions

File tree

.github/workflows/ci.yml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
jobs:
10+
test:
11+
strategy:
12+
matrix:
13+
go-version: ['1.25', '1.26']
14+
os: [ubuntu-latest, macos-latest]
15+
runs-on: ${{ matrix.os }}
16+
17+
steps:
18+
- uses: actions/checkout@v4
19+
20+
- uses: actions/setup-go@v5
21+
with:
22+
go-version: ${{ matrix.go-version }}
23+
24+
- name: Vet
25+
run: go vet ./...
26+
27+
- name: Test
28+
run: go test -race -count=3 -timeout=60s ./...
29+
30+
- name: Benchmark
31+
run: go test -bench=. -benchmem -benchtime=1s ./...

.gitignore

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Binaries
2+
*.exe
3+
*.exe~
4+
*.dll
5+
*.so
6+
*.dylib
7+
*.test
8+
9+
# Go build cache
10+
*.o
11+
*.a
12+
13+
# IDE
14+
.idea/
15+
.vscode/
16+
*.swp
17+
*.swo
18+
*~
19+
20+
# OS
21+
.DS_Store
22+
Thumbs.db
23+
24+
# Coverage
25+
coverage.out
26+
coverage.html

BENCHMARKS.md

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
# Benchmarks
2+
3+
Comparative benchmarks of `imcache` against popular Go cache libraries.
4+
5+
Benchmark source code lives in [\_benchmark/](./_benchmark/).
6+
Inspired by [bool64/cache benchmarks](https://github.com/bool64/cache/blob/master/_benchmark/README.md).
7+
8+
## Test environment
9+
10+
- **CPU:** Apple M1 Max (10 cores)
11+
- **OS:** macOS (darwin/arm64)
12+
- **Go:** 1.26
13+
- **GOMAXPROCS:** 10
14+
15+
## Libraries tested
16+
17+
| Library | Type safety | Eviction | Locking strategy |
18+
|---------|-------------|----------|------------------|
19+
| **imcache** | Generics | TTL + LRU | Sharded RWMutex (256 shards) |
20+
| **imcache (LRU)** | Generics | TTL + LRU | Same, with per-shard capacity limits |
21+
| [sync.Map](https://pkg.go.dev/sync#Map) | `any` | None | Lock-free reads (stdlib) |
22+
| mutexMap | N/A | None | Single `sync.Mutex` + `map` |
23+
| rwMutexMap | N/A | None | Single `sync.RWMutex` + `map` |
24+
| [go-cache](https://github.com/patrickmn/go-cache) | `any` | TTL | Single global RWMutex |
25+
| [golang-lru](https://github.com/hashicorp/golang-lru) | Generics | LRU | Single global Mutex |
26+
| [bigcache](https://github.com/allegro/bigcache) | `[]byte` | TTL | Sharded, pre-allocated ring buffers |
27+
| [freecache](https://github.com/coocood/freecache) | `[]byte` | LRU + TTL | Segmented, pre-allocated |
28+
29+
**Notes on fairness:**
30+
31+
- Byte-oriented caches (`bigcache`, `freecache`) use pre-computed `[]byte` keys and values in the benchmark to avoid penalizing them for string-to-byte conversion.
32+
- `sync.Map` is purpose-built for read-heavy workloads with stable keysets. It provides no TTL, no eviction, and no type safety. It is included as a performance ceiling for reads, not as a direct competitor.
33+
- `golang-lru` uses a single mutex for all operations, which means every `Get` takes an exclusive lock (to update LRU order). This hurts it under concurrency.
34+
35+
---
36+
37+
## Concurrent throughput
38+
39+
10,000 pre-loaded items. All goroutines (GOMAXPROCS=10) run in parallel, performing reads and writes at the specified ratio.
40+
41+
### Results (ns/op, lower is better)
42+
43+
```
44+
0% writes 0.1% writes 1% writes 10% writes 50% writes
45+
46+
imcache 65.60 65.93 65.67 60.59 37.45
47+
imcache_lru 71.87 81.62 74.98 75.31 52.98
48+
sync.Map 3.14 3.33 4.52 9.94 25.83
49+
mutexMap 151.1 151.2 150.8 163.7 207.4
50+
rwMutexMap 121.8 88.72 68.36 56.58 99.31
51+
go-cache 123.9 99.81 78.20 64.93 123.4
52+
golang-lru 199.3 196.0 195.5 193.9 211.8
53+
bigcache 27.96 27.24 32.44 40.32 63.71
54+
freecache 54.22 51.78 51.79 55.70 63.81
55+
```
56+
57+
### Allocations per operation
58+
59+
```
60+
0% writes 10% writes 50% writes
61+
62+
imcache 0 0 0
63+
imcache_lru 0 0 0
64+
sync.Map 0 0 1
65+
mutexMap 0 0 0
66+
rwMutexMap 0 0 0
67+
go-cache 0 0 0
68+
golang-lru 0 0 0
69+
bigcache 2 1 1
70+
freecache 1 0 0
71+
```
72+
73+
### What this tells us
74+
75+
**Where imcache does well:**
76+
77+
- 2x faster than `go-cache` under pure reads (66ns vs 124ns). The sharded locking pays for itself immediately once there is any concurrency.
78+
- Under heavy writes (50%), `imcache` at 37ns is the fastest typed cache in the set, beating `go-cache` (123ns) by 3.3x and `golang-lru` (212ns) by 5.7x.
79+
- Zero allocations across every write ratio. No other sharded cache in this benchmark achieves that.
80+
- Stable latency: `imcache` barely changes across write ratios (60-66ns for reads, 37ns at 50% writes), while `go-cache` and `rwMutexMap` fluctuate significantly as the read/write mix shifts.
81+
- With LRU enabled, `imcache_lru` still outperforms `go-cache` and `golang-lru` at every write ratio despite the extra bookkeeping.
82+
83+
**Where imcache loses:**
84+
85+
- `sync.Map` is 20x faster for pure reads (3ns vs 66ns). This is expected. `sync.Map` uses a lock-free read path optimized for stable keys that are written once and read many times. It has no hashing, no sharding indirection, and no expiry checks. It is not a general-purpose cache.
86+
- `bigcache` is about 2.4x faster for pure reads (28ns vs 66ns). `bigcache` is a mature, heavily optimized byte-oriented cache with its own sharded design. The trade-off is that it only stores `[]byte` values (no generics, no type safety), allocates on every operation (2 allocs/op for reads), and uses significantly more memory (see below).
87+
- `freecache` is slightly faster for pure reads (54ns vs 66ns) for similar reasons: it operates on raw bytes and avoids the Go type system.
88+
- Single-threaded, `imcache` (28ns) is slower than a plain `map` behind a `sync.Mutex` (16ns). The sharding overhead (hash computation + pointer indirection) costs about 12ns per operation. This overhead only pays off under concurrency.
89+
90+
**The broader picture:**
91+
92+
Among caches that offer type safety, TTL support, and eviction policies, `imcache` is the fastest in this benchmark set at every concurrency level. The libraries that beat it on raw throughput (`sync.Map`, `bigcache`, `freecache`) each sacrifice one or more of: type safety, eviction control, or memory efficiency.
93+
94+
---
95+
96+
## Single-thread read performance
97+
98+
10,000 items, single goroutine, no contention. This isolates per-operation overhead without any locking effects.
99+
100+
```
101+
ns/op allocs/op
102+
103+
imcache 27.70 0
104+
imcache_lru 47.00 0
105+
sync.Map 23.60 0
106+
mutexMap 15.81 0
107+
rwMutexMap 15.61 0
108+
go-cache 17.54 0
109+
golang-lru 29.53 0
110+
bigcache 76.98 2
111+
freecache 102.8 1
112+
```
113+
114+
Without contention, the ranking changes. A raw `map` behind a mutex is the fastest (16ns) because there is zero contention and no sharding overhead. `imcache` at 28ns sits between `sync.Map` (24ns) and `golang-lru` (30ns).
115+
116+
`bigcache` and `freecache` are the slowest in single-threaded reads because their byte-oriented storage requires hashing, segment lookup, and buffer scanning even for a single reader.
117+
118+
---
119+
120+
## Memory usage
121+
122+
1,000,000 string key-value pairs loaded into each cache. Heap in-use measured via `runtime.ReadMemStats` after forcing GC.
123+
124+
```
125+
Cache MB/inuse
126+
127+
imcache 103.6 MB
128+
sync.Map 136.2 MB
129+
go-cache 111.7 MB
130+
golang-lru 148.9 MB
131+
bigcache 3019.5 MB *
132+
freecache 288.6 MB
133+
```
134+
135+
\* `bigcache` pre-allocates ring buffers per shard. The 3 GB figure reflects the benchmark configuration (`MaxEntriesInWindow = 10M`). Real-world usage with tuned settings will use less, but bigcache will always use more memory than map-based caches due to its pre-allocation strategy.
136+
137+
**Observations:**
138+
139+
- `imcache` has the lowest memory footprint at 104 MB, which is 7% less than `go-cache` (112 MB) and 24% less than `sync.Map` (136 MB).
140+
- `golang-lru` uses 149 MB because it maintains a doubly-linked list alongside the map (each entry has a list element with two pointers plus interface boxing).
141+
- `freecache` at 289 MB pre-allocates a contiguous byte buffer, which avoids GC pressure but costs more upfront memory.
142+
- `imcache` achieves its low footprint because each entry is a single struct with a string key, the value, an int64 expiry timestamp, and an optional list pointer (nil when LRU is disabled).
143+
144+
---
145+
146+
## How to reproduce
147+
148+
```bash
149+
cd _benchmark
150+
151+
# Quick run (single iteration, ~4 minutes)
152+
go test -bench=. -benchmem -benchtime=3s -timeout=300s ./...
153+
154+
# Stable results (10 iterations for statistical analysis, ~40 minutes)
155+
go test -bench=. -benchmem -benchtime=3s -count=10 -timeout=600s ./... > report.txt
156+
benchstat report.txt
157+
158+
# Memory usage only
159+
go test -v -run TestMemoryUsage ./...
160+
```
161+
162+
## Limitations
163+
164+
- All benchmarks run on a single machine. Results will differ on other hardware, especially machines with different core counts or cache line sizes.
165+
- The benchmark uses a fixed set of 10,000 pre-loaded keys. Real workloads with different key distributions, value sizes, or hit/miss ratios may produce different relative rankings.
166+
- `sync.Map` performance depends heavily on the read/write ratio and key stability. The numbers here reflect its best case (stable keys, read-heavy).
167+
- Memory measurements are point-in-time snapshots after GC. Actual runtime memory usage will fluctuate depending on allocation patterns and GC timing.
168+
- `bigcache` memory usage is highly configuration-dependent. The number shown here is not representative of a tuned production deployment.

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 Prasad (Audiomob)
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.

0 commit comments

Comments
 (0)