All Benchmarks

Go Benchmark

Slice Preallocation

How much does preallocating slice capacity actually save?

sliceappendpreallocationmakeallocationmemory

Growing a slice with append from nil looks harmless, but every time the backing array runs out of capacity the runtime allocates a bigger one and copies all existing elements over. This benchmark builds a slice of 10,000 integers three ways: appending to a nil slice, appending into a slice preallocated with make([]int, 0, n), and writing by index into make([]int, n). The allocation counts tell the story — the nil-slice version re-allocates and copies over a dozen times, while the preallocated versions allocate exactly once. If you know the size up front (even approximately), tell the runtime.

linux/amd64AMD Ryzen 9 9950X3D 16-Core Processorbenchmarks/slice-prealloc
Compare atCPUs

1 CPU

Fastest

Make With Length

5.3 µs/op

Slowest

Append Nil Slice

19.15 µs/op · 3.6x slower

32 CPUs

Fastest

Append With Capacity

15.79 µs/op

Slowest

Append Nil Slice

67.28 µs/op · 4.3x slower

Performance Comparison (lower is better)
CPU:
#1

Make With Length #

Fastest (1 CPU)

Allocates make([]int, sliceSize) and assigns elements by index. One allocation like the capacity variant, but also skips append's per-call length/capacity bookkeeping. The trade-off: the slice starts at full length, so a bug that skips an index silently leaves a zero value instead of a shorter slice.

CPU Scaling (lower is better)
// sliceSize is the number of elements each implementation builds per iteration.
const sliceSize = 10_000

func BenchmarkMakeWithLength_run(b *testing.B) {
	for i := 0; i < b.N; i++ {
		s := make([]int, sliceSize)
		for j := 0; j < sliceSize; j++ {
			s[j] = j
		}
		sink = s[len(s)-1]
	}
}
1 CPU
3.6xfaster(262%)thanAppend Nil Slice
1xfaster(1%)thanAppend With Capacity
32 CPUs
4.2xfaster(321%)thanAppend Nil Slice
1xslower(1%)thanAppend With Capacity
#2

Append With Capacity #

Fastest (32 CPUs)

Preallocates the backing array with make([]int, 0, sliceSize) and appends into it. The append calls never outgrow capacity, so there is exactly one allocation and zero copies — while keeping the convenient append API and correct length tracking.

CPU Scaling (lower is better)
// sliceSize is the number of elements each implementation builds per iteration.
const sliceSize = 10_000

func BenchmarkAppendWithCapacity_run(b *testing.B) {
	for i := 0; i < b.N; i++ {
		s := make([]int, 0, sliceSize)
		for j := 0; j < sliceSize; j++ {
			s = append(s, j)
		}
		sink = s[len(s)-1]
	}
}
1 CPU
3.6xfaster(260%)thanAppend Nil Slice
1xslower(1%)thanMake With Length
32 CPUs
4.3xfaster(326%)thanAppend Nil Slice
1xfaster(1%)thanMake With Length
#3

Append Nil Slice #

Slowest

Starts with a nil slice and lets append grow it. Each time capacity is exhausted the runtime allocates a larger backing array (roughly doubling for small slices, growing ~25% for large ones) and copies every element over. Building 10,000 elements this way triggers a whole series of allocations and copies.

CPU Scaling (lower is better)
// sliceSize is the number of elements each implementation builds per iteration.
const sliceSize = 10_000

func BenchmarkAppendNilSlice_run(b *testing.B) {
	for i := 0; i < b.N; i++ {
		var s []int
		for j := 0; j < sliceSize; j++ {
			s = append(s, j)
		}
		sink = s[len(s)-1]
	}
}
1 CPU
3.6xslower(260%)thanAppend With Capacity
3.6xslower(262%)thanMake With Length
32 CPUs
4.3xslower(326%)thanAppend With Capacity
4.2xslower(321%)thanMake With Length

Contributors