All Benchmarks

Go Benchmark

Counter

Compare plain int, atomic, and mutex-based counters for increments and reads.

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 atCPUs

1 CPU

Get

Fastest

Int Counter

0.23 ns/op

Slowest

Int Counter With Mutex

8.54 ns/op · 37x slower

Increment

Fastest

Int Counter

0.25 ns/op

Slowest

Int Counter With Mutex

8.67 ns/op · 35x slower

32 CPUs

Get

Fastest

Atomic Uint Counter

0.25 ns/op

Slowest

Int Counter With Mutex

8.54 ns/op · 35x slower

Increment

Fastest

Int Counter

0.26 ns/op

Slowest

Int Counter With Mutex

8.92 ns/op · 34x slower

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. It serves as a baseline to show the raw cost of incrementing without any synchronization.

Performance (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

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

Increment

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

Get

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

Increment

13.4xfaster(1238%)thanAtomic Pointer Counter
13.4xfaster(1238%)thanAtomic Uint Counter
33.7xfaster(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 (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
1xslower(2%)thanInt Counter
36.6xfaster(3555%)thanInt Counter With Mutex

Increment

Same speed asAtomic Uint Counter
14.2xslower(1324%)thanInt Counter
2.5xfaster(146%)thanInt Counter With Mutex
32 CPUs

Get

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

Increment

Same speed asAtomic Uint Counter
13.4xslower(1238%)thanInt Counter
2.5xfaster(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 (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

1xslower(2%)thanInt Counter
36.4xfaster(3538%)thanInt Counter With Mutex

Increment

14.2xslower(1320%)thanInt Counter
2.5xfaster(146%)thanInt Counter With Mutex
32 CPUs

Get

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

Increment

13.4xslower(1238%)thanInt Counter
2.5xfaster(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 (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.6xslower(3555%)thanAtomic Pointer Counter
36.4xslower(3538%)thanAtomic Uint Counter
37.2xslower(3617%)thanInt Counter

Increment

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

Get

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

Increment

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

Contributors