All Benchmarks

Go Benchmark

sort.Slice vs slices.Sort

The old sort package against the generic slices package — same algorithm, different dispatch.

sortslicesgenericssort-slicesort-funcstdlib

Go now ships four ways to sort a slice: the classic sort.Sort with a hand-written sort.Interface, the closure-based sort.Slice, and the generic slices.Sort and slices.SortFunc added in Go 1.21. They use closely related pattern-defeating quicksort algorithms, so the interesting difference is dispatch: interface method calls and closures cannot be fully inlined and box their data, while the generic functions are compiled for the concrete element type. This benchmark sorts the same shuffled 1000-element int slice with each API. If you are still reaching for sort.Slice out of habit, the chart shows what switching to slices.Sort buys.

linux/amd64AMD Ryzen 9 9950X3D 16-Core Processorbenchmarks/sort-slices-vs-sort
Compare atCPUs

1 CPU

Fastest

Slices Sort

4.45 µs/op

Slowest

Sort Slice

14.96 µs/op · 3.4x slower

32 CPUs

Fastest

Slices Sort

4.43 µs/op

Slowest

Sort Slice

14.76 µs/op · 3.3x slower

Performance Comparison (lower is better)
CPU:
#1

Slices Sort #

Fastest

slices.Sort(work) — the generic sort for ordered element types. Comparisons compile down to plain < on the concrete type with no function calls at all, which is why it is consistently the fastest way to sort a slice in Go.

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

// makeUnsorted returns a deterministic pseudo-random permutation to sort.
func makeUnsorted() []int {
	r := rand.New(rand.NewPCG(42, 1024))
	return r.Perm(sliceSize)
}

func BenchmarkSlicesSort_run(b *testing.B) {
	data := makeUnsorted()
	work := make([]int, len(data))
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		copy(work, data)
		slices.Sort(work)
	}
	sink = work[0]
}
1 CPU
3.2xfaster(224%)thanSlices Sort Func
3.3xfaster(230%)thanSort Interface
3.4xfaster(236%)thanSort Slice
32 CPUs
3.2xfaster(222%)thanSlices Sort Func
3.3xfaster(230%)thanSort Interface
3.3xfaster(233%)thanSort Slice
#2

Slices Sort Func #

slices.SortFunc(work, cmp) — the generic replacement for sort.Slice. The slice is accessed directly at its concrete type (no reflection, no boxing), but every comparison still goes through the provided function, which limits inlining.

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

// makeUnsorted returns a deterministic pseudo-random permutation to sort.
func makeUnsorted() []int {
	r := rand.New(rand.NewPCG(42, 1024))
	return r.Perm(sliceSize)
}

func BenchmarkSlicesSortFunc_run(b *testing.B) {
	data := makeUnsorted()
	work := make([]int, len(data))
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		copy(work, data)
		slices.SortFunc(work, func(x, y int) int { return cmp.Compare(x, y) })
	}
	sink = work[0]
}
1 CPU
3.2xslower(224%)thanSlices Sort
1xfaster(2%)thanSort Interface
1xfaster(4%)thanSort Slice
32 CPUs
3.2xslower(222%)thanSlices Sort
1xfaster(3%)thanSort Interface
1xfaster(4%)thanSort Slice
#3

Sort Interface #

sort.Sort(IntSlice(work)) with a hand-written Len/Less/Swap implementation. Avoids reflection, but every single comparison and swap is an interface method call — the dispatch cost the generic APIs were designed to eliminate.

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

// makeUnsorted returns a deterministic pseudo-random permutation to sort.
func makeUnsorted() []int {
	r := rand.New(rand.NewPCG(42, 1024))
	return r.Perm(sliceSize)
}

// IntSlice implements sort.Interface for a slice of ints.
type IntSlice []int

func (s IntSlice) Len() int           { return len(s) }
func (s IntSlice) Less(i, j int) bool { return s[i] < s[j] }
func (s IntSlice) Swap(i, j int)      { s[i], s[j] = s[j], s[i] }

func BenchmarkSortInterface_run(b *testing.B) {
	data := makeUnsorted()
	work := make([]int, len(data))
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		copy(work, data)
		sort.Sort(IntSlice(work))
	}
	sink = work[0]
}
1 CPU
3.3xslower(230%)thanSlices Sort
1xslower(2%)thanSlices Sort Func
1xfaster(2%)thanSort Slice
32 CPUs
3.3xslower(230%)thanSlices Sort
1xslower(3%)thanSlices Sort Func
1xfaster(1%)thanSort Slice
#4

Sort Slice #

Slowest

sort.Slice(work, func(i, j int) bool { ... }) — the pre-generics convenience API. Internally it uses reflection (reflect.Swapper) for swapping elements and calls the comparison closure through a function pointer, both of which add per-element overhead the compiler cannot optimize away.

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

// makeUnsorted returns a deterministic pseudo-random permutation to sort.
func makeUnsorted() []int {
	r := rand.New(rand.NewPCG(42, 1024))
	return r.Perm(sliceSize)
}

func BenchmarkSortSlice_run(b *testing.B) {
	data := makeUnsorted()
	work := make([]int, len(data))
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		copy(work, data)
		sort.Slice(work, func(i, j int) bool { return work[i] < work[j] })
	}
	sink = work[0]
}
1 CPU
3.4xslower(236%)thanSlices Sort
1xslower(4%)thanSlices Sort Func
1xslower(2%)thanSort Interface
32 CPUs
3.3xslower(233%)thanSlices Sort
1xslower(4%)thanSlices Sort Func
1xslower(1%)thanSort Interface

Contributors