Skip to content
SagarCodeDevPublic

About

playground for go benchmarks

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 

Repository files navigation

go-play

A Go sandbox for re-running published benchmarks and dissecting why A beats B — not just on paper, but at the level of cache lines, allocations, and syscalls.

Most benchmark blog posts hand you a verdict: "X is 3× faster than Y." This repo is the opposite. It's a workshop where each experiment is reproduced from scratch, dialed in across a range of inputs, and profiled until the systems-level reason behind the number is obvious.


Why this exists

Reading a benchmark and believing a benchmark are different things. The goal here is to:

  • Reproduce the published result locally, on real hardware.
  • Stress it across input sizes, access patterns, and concurrency until the curve breaks.
  • Explain the delta through CPU cache behaviour, allocations, branch prediction, syscall cost, GC pressure, etc.
  • Generalise the takeaway into a rule of thumb worth carrying into production code.

Every folder under play/ is one such investigation.


Current experiments

play/fetcher.go — Map vs Slice for small-N CRUD

A CRUDBase interface implemented two ways:

Impl Backing store Get Add Delete
CustomMap map[string]any O(1) hash O(1) hash O(1) hash
CustomSlice []any O(N) scan O(1) append O(N) shift

The question. For very small N (think 5–50 elements), the textbook says "use a map". Production code disagrees often enough — a linear slice scan beats a hash map until N gets big enough that the constant-factor advantage of cache-friendly contiguous memory loses to algorithmic complexity. Where does that crossover actually sit?

What we're hunting:

  • The crossover N where map overtakes slice.
  • The cost of runtime.mapaccess (hashing + pointer chasing) vs a tight loop over contiguous memory the prefetcher loves.
  • Allocation differences (map grows in buckets; slice doubles).
  • How interface{} boxing skews both.

Run it:

go test -bench=. -benchmem -benchtime=5s ./play/...

Repo layout

go-play/
├── go.mod
└── play/
    ├── fetcher.go        # experiment subject (map vs slice CRUD)
    └── fetcher_test.go   # benchmark harness

Each new investigation should live in its own subpackage under play/ with:

  1. A short doc comment at the top of the file stating the claim being tested and the source (blog post, paper, talk).
  2. The minimal implementation(s) under comparison — no production noise.
  3. A _test.go with Benchmark* functions covering at least 3 input sizes.
  4. A note in this README under Current experiments with the hypothesis and the answer once known.

How to run anything

# Single benchmark, verbose
go test -bench=BenchmarkX -benchmem -benchtime=5s ./play/...

# CPU + memory profiles
go test -bench=. -cpuprofile=cpu.out -memprofile=mem.out ./play/...
go tool pprof -http=:8080 cpu.out

# Compare two implementations with benchstat
go install golang.org/x/perf/cmd/benchstat@latest
go test -bench=. -count=10 ./play/... > old.txt
# ...make a change...
go test -bench=. -count=10 ./play/... > new.txt
benchstat old.txt new.txt

# Inspect generated assembly for a hot path
go build -gcflags="-S" ./play/... 2>&1 | less

# Escape analysis — see what's hitting the heap
go build -gcflags="-m -m" ./play/... 2>&1 | less

Backlog of experiments worth doing

Things to pull in next, each with a clear "A vs B, why" framing:

  • sync.Map vs RWMutex + map — at what read/write ratio does sync.Map actually win?
  • Channels vs mutexes for producer/consumer — the famous "share memory by communicating" tradeoff, quantified.
  • strings.Builder vs += vs bytes.Buffer — where does each one's overhead come from?
  • Goroutine pools vs unbounded spawning — when does scheduler contention dominate?
  • json.Marshal vs easyjson vs sonic — reflection cost, codegen wins, SIMD on hot fields.
  • atomic vs mutex for counters — cache-line ping-pong under contention.
  • Slice preallocation — make([]T, 0, N) vs growing — measure realloc + copy cost.

Ground rules

  • Every benchmark must be reproducible by a stranger with go test -bench=....
  • No production patterns until the experiment shows they earn their weight.
  • The README is the source of truth for what was learned, not the commit history.
  • When the result contradicts the blog post that inspired it, write down why — that's the whole point.

About

playground for go benchmarks

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages