Skip to content

Latest commit

 

History

History
83 lines (67 loc) · 4.06 KB

File metadata and controls

83 lines (67 loc) · 4.06 KB

syncx benchmarks

Comparative benchmarks between syncx and the equivalent primitive from the most widely used library for each problem in the Go ecosystem:

Primitive Compared against
Semaphore golang.org/x/sync/semaphore (Weighted, used as a binary semaphore)
Pool golang.org/x/sync/errgroup (SetLimit)
CircuitBreaker github.com/sony/gobreaker/v2
Retry github.com/cenkalti/backoff/v4

This directory is its own Go module with its own go.mod, replaced to the parent directory. That keeps these three external dependencies out of the main syncx module — go get github.com/Ismahsantiago/syncx still pulls in zero dependencies, since Go module tooling skips nested modules that declare their own go.mod.

Running

cd bench
go test -run=^$ -bench=. -benchmem ./...

Add -count=10 and pipe through benchstat for statistically sound comparisons before drawing conclusions:

go test -run=^$ -bench=. -benchmem -count=10 ./... > new.txt
go run golang.org/x/perf/cmd/benchstat@latest new.txt

Reference results

Captured on an Apple M1 Pro (darwin/arm64, Go 1.25), -benchtime=1s -count=3, variance under 2% across runs. Numbers will differ on other hardware — re-run locally rather than trusting these as absolute, but the relative shape has been stable across repeated runs on this machine.

BenchmarkSyncxCircuitBreaker-8         27.0 ns/op      0 B/op   0 allocs/op
BenchmarkGobreakerCircuitBreaker-8     98.6 ns/op      0 B/op   0 allocs/op

BenchmarkSyncxRetry-8                   3.1 ns/op      0 B/op   0 allocs/op
BenchmarkCenkaltiBackoffRetry-8        46.5 ns/op     48 B/op   3 allocs/op

BenchmarkSyncxSemaphore-8             152.8 ns/op      0 B/op   0 allocs/op
BenchmarkXSyncSemaphore-8             228.4 ns/op    175 B/op   2 allocs/op
BenchmarkSyncxSemaphoreTryAcquire-8    23.0 ns/op      0 B/op   0 allocs/op
BenchmarkXSyncSemaphoreTryAcquire-8    27.0 ns/op      0 B/op   0 allocs/op
BenchmarkSyncxSemaphoreMutexBaseline  128.0 ns/op      0 B/op   0 allocs/op

BenchmarkSyncxPool-8                  594.7 µs/op   8.4 KiB   7 allocs/op   (1000 jobs, 8 workers)
BenchmarkErrgroupPool-8               558.5 µs/op  47.2 KiB   2005 allocs/op (1000 jobs, 8 workers)

Reading these numbers

  • CircuitBreaker, Retry, Semaphore: syncx is faster and allocation-free where the comparison library allocates. This is mostly architectural, not a tuning trick — gobreaker and backoff are built around interfaces and closures with more indirection per call, and x/sync/semaphore's Weighted supports arbitrary weights (a heap of waiters), which costs more than syncx's fixed-capacity channel when only ever acquiring 1. If you need weighted acquisition, x/sync/semaphore is the right tool and this comparison does not apply.
  • Pool vs errgroup: wall-clock time is a near tie — both are bound by the same 8-worker fan-out over 1000 trivial jobs, so scheduler overhead dominates either way. The real difference is allocations: Pool reuses a fixed set of worker goroutines and a shared results channel (7 allocations total for the whole run), while errgroup.Go spawns one goroutine per call (2005 allocations for 1000 jobs). Prefer errgroup when you just need bounded fan-out with first-error cancellation and no per-job result; prefer Pool when you need a Result per job (value and error, paired with its input) streamed back as work completes.

None of this means "always use syncx" — it means syncx's primitives make narrower promises (fixed capacity, single output type, linear backoff) than their more general counterparts, and narrower promises are cheaper to keep. Pick based on the guarantees you actually need; see the root README for what each primitive promises.