All Benchmarks

Counter — Go Benchmark

A benchmark to compare the performance of different counter implementations in Go.

counteratomicmutexconcurrency

Compares different ways to implement a counter in Go, from a plain integer to thread-safe variants using atomics or a mutex. The basic int counter serves as a non-synchronized baseline, while atomic.Uint64, atomic.AddUint64, and sync.Mutex each add safety at different performance costs.

linux/amd64AMD Ryzen 9 9950X3D 16-Core Processorbenchmarks/counter
Compare at
CPUs
Performance Comparison (lower is better)
CPU:

Int Counter

Fastest (Get, 1 CPU)Fastest (Increment)

A plain uint64 counter incremented with c.count++. Not thread-safe — serves as a baseline to show the raw cost of incrementing without any synchronization.

Performance — Int Counter (lower is better)
CPU:
type IntCounter struct {
	count uint64
}

func (c *IntCounter) increment() {
	c.count++
}

func (c *IntCounter) get() uint64 {
	return c.count
}

func BenchmarkIntCounter_increment(b *testing.B) {
	var counter IntCounter

	for i := 0; i < b.N; i++ {
		counter.increment()
	}
}

func BenchmarkIntCounter_get(b *testing.B) {
	var counter IntCounter

	for i := 0; i < b.N; i++ {
		intCounterSink = counter.get()
	}
}
1 CPU

Get

1×faster(2%)thanAtomic Pointer Counter
1×faster(2%)thanAtomic Uint Counter
37.2×faster(3617%)thanInt Counter With Mutex

Increment

14.2×faster(1324%)thanAtomic Pointer Counter
14.2×faster(1320%)thanAtomic Uint Counter
35×faster(3397%)thanInt Counter With Mutex
32 CPUs

Get

Same speed asAtomic Uint Counter
34.4×faster(3340%)thanInt Counter With Mutex

Increment

13.4×faster(1238%)thanAtomic Pointer Counter
13.4×faster(1238%)thanAtomic Uint Counter
33.7×faster(3273%)thanInt Counter With Mutex

Atomic Pointer Counter

Uses a raw uint64 field and the free-function atomic.AddUint64 / atomic.LoadUint64 API, passing a pointer to the field explicitly. This is the pre-Go-1.19 style of atomic access.

Performance — Atomic Pointer Counter (lower is better)
CPU:
type AtomicPointerCounter struct {
	count uint64
}

func (c *AtomicPointerCounter) increment() {
	atomic.AddUint64(&c.count, 1)
}

func (c *AtomicPointerCounter) get() uint64 {
	return atomic.LoadUint64(&c.count)
}

func BenchmarkAtomicPointerCounter_increment(b *testing.B) {
	var counter AtomicPointerCounter

	for i := 0; i < b.N; i++ {
		counter.increment()
	}
}

func BenchmarkAtomicPointerCounter_get(b *testing.B) {
	var counter AtomicPointerCounter

	for i := 0; i < b.N; i++ {
		atomicPointerCounterSink = counter.get()
	}
}
1 CPU

Get

Same speed asAtomic Uint Counter
1×slower(2%)thanInt Counter
36.6×faster(3555%)thanInt Counter With Mutex

Increment

Same speed asAtomic Uint Counter
14.2×slower(1324%)thanInt Counter
2.5×faster(146%)thanInt Counter With Mutex
32 CPUs

Get

Same speed asAtomic Uint Counter
Same speed asInt Counter
34.4×faster(3344%)thanInt Counter With Mutex

Increment

Same speed asAtomic Uint Counter
13.4×slower(1238%)thanInt Counter
2.5×faster(152%)thanInt Counter With Mutex

Atomic Uint Counter

Fastest (Get, 32 CPUs)

Uses atomic.Uint64 (introduced in Go 1.19) which wraps atomic operations behind method calls (Add, Load). Functionally equivalent to the pointer variant but with a cleaner API.

Performance — Atomic Uint Counter (lower is better)
CPU:
type AtomicUintCounter struct {
	count atomic.Uint64
}

func (c *AtomicUintCounter) increment() {
	c.count.Add(1)
}

func (c *AtomicUintCounter) get() uint64 {
	return c.count.Load()
}

func BenchmarkAtomicUintCounter_increment(b *testing.B) {
	var counter AtomicUintCounter

	for i := 0; i < b.N; i++ {
		counter.increment()
	}
}

func BenchmarkAtomicUintCounter_get(b *testing.B) {
	var counter AtomicUintCounter

	for i := 0; i < b.N; i++ {
		atomicUintCounterSink = counter.get()
	}
}
1 CPU

Get

1×slower(2%)thanInt Counter
36.4×faster(3538%)thanInt Counter With Mutex

Increment

14.2×slower(1320%)thanInt Counter
2.5×faster(146%)thanInt Counter With Mutex
32 CPUs

Get

Same speed asInt Counter
34.5×faster(3353%)thanInt Counter With Mutex

Increment

13.4×slower(1238%)thanInt Counter
2.5×faster(152%)thanInt Counter With Mutex

Int Counter With Mutex

Slowest (Get)Slowest (Increment)

Wraps a plain uint64 counter with a sync.Mutex, locking on every increment and get call. Thread-safe, but the mutex adds overhead compared to lock-free atomic operations.

Performance — Int Counter With Mutex (lower is better)
CPU:
type IntCounterWithMutex struct {
	count uint64
	mu    sync.Mutex
}

func (c *IntCounterWithMutex) increment() {
	c.mu.Lock()
	defer c.mu.Unlock()
	c.count++
}

func (c *IntCounterWithMutex) get() uint64 {
	c.mu.Lock()
	defer c.mu.Unlock()
	return c.count
}

func BenchmarkIntCounterWithMutex_increment(b *testing.B) {
	var counter IntCounterWithMutex

	for i := 0; i < b.N; i++ {
		counter.increment()
	}
}

func BenchmarkIntCounterWithMutex_get(b *testing.B) {
	var counter IntCounterWithMutex

	for i := 0; i < b.N; i++ {
		intCounterWithMutexSink = counter.get()
	}
}
1 CPU

Get

36.6×slower(3555%)thanAtomic Pointer Counter
36.4×slower(3538%)thanAtomic Uint Counter
37.2×slower(3617%)thanInt Counter

Increment

2.5×slower(146%)thanAtomic Pointer Counter
2.5×slower(146%)thanAtomic Uint Counter
35×slower(3397%)thanInt Counter
32 CPUs

Get

34.4×slower(3344%)thanAtomic Pointer Counter
34.5×slower(3353%)thanAtomic Uint Counter
34.4×slower(3340%)thanInt Counter

Increment

2.5×slower(152%)thanAtomic Pointer Counter
2.5×slower(152%)thanAtomic Uint Counter
33.7×slower(3273%)thanInt Counter

Contributors