Which function should you use to sort a slice?

the korean guy
3 min readJul 22, 2024

--

Today, I want to explore different ways to sort a integer slice in Go.

We will compare three functions: slices.Sort, slices.SortFunc, and sort.Slice.

First, let’s take a look at the benchmark results.


func BenchmarkSortSlice(b *testing.B) {
b.ReportAllocs()
b.StopTimer()
arr := make([]int, 1000000)
for i := 0; i < 1000000; i++ {
arr[i] = rand.Intn(1000)
}
b.StartTimer()
for i := 0; i < b.N; i++ {
sort.Slice(arr, func(i, j int) bool {
return arr[i] < arr[j]
})
}
}

func BenchmarkSlicesSortFunc(b *testing.B) {
b.ReportAllocs()
b.StopTimer()
arr := make([]int, 1000000)
for i := 0; i < 1000000; i++ {
arr[i] = rand.Intn(1000)
}
b.StartTimer()
for i := 0; i < b.N; i++ {
slices.SortFunc(arr, func(a, b int) int {
return a - b
})
}
}

func BenchmarkSlicesSort(b *testing.B) {
b.ReportAllocs()
b.StopTimer()
arr := make([]int, 1000000)
for i := 0; i < 1000000; i++ {
arr[i] = rand.Intn(1000)
}
b.StartTimer()
for i := 0; i < b.N; i++ {
slices.Sort(arr)
}
}

// cpu: Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
// BenchmarkSortSlice
// BenchmarkSortSlice-12 405 2796977 ns/op 56 B/op 2 allocs/op
// BenchmarkSlicesSortFunc
// BenchmarkSlicesSortFunc-12 535 2215764 ns/op 0 B/op 0 allocs/op
// BenchmarkSlicesSort
// BenchmarkSlicesSort-12 1105 1058691 ns/op 0 B/op 0 allocs/op 0 allocs/op

As you can see, when sorting an integer slice, slices.Sort performs the best, followed by slices.SortFunc, and then sort.Slice.

All of these functions are included in Go’s standard packages, but why is there a difference in performance?

sort.Slice vs slices.SortFunc

First, let’s compare sort.Slice and slices.SortFunc, both of which allow sorting with a custom function.

// sort.Slice
func Slice(x any, less func(i, j int) bool) {
rv := reflectlite.ValueOf(x)
swap := reflectlite.Swapper(x)
length := rv.Len()
limit := bits.Len(uint(length))
pdqsort_func(lessSwap{less, swap}, 0, length, limit)
}

// slices.SortFunc
func SortFunc[S ~[]E, E any](x S, cmp func(a, b E) int) {
n := len(x)
pdqsortCmpFunc(x, 0, n, bits.Len(uint(n)), cmp)
}

Both functions use the pdqsort algorithm.

The difference is that sort.Slice uses the any keyword, while slices.SortFunc utilizes generics.

In my view, the only difference lies in reflectlite.ValueOf(x) and reflectlite.Swapper(x). It appears that these two methods internally call functions from the unsafe package, which may potentially lead to heap memory allocations. Allocating memory on the heap is a task with considerable overhead, which likely contributes to the observed performance degradation.

On the other hand, slices.SortFunc avoids this overhead because generics provide type safety, eliminating the need for such processes.

Functions that use generics are generated at compile time, so they don’t require reflection and thus avoid heap memory allocation. This results in better performance.

So, what is the difference between slices.SortFunc and slices.Sort?

// slices.Sort
func Sort[S ~[]E, E cmp.Ordered](x S) {
n := len(x)
pdqsortOrdered(x, 0, n, bits.Len(uint(n)))
}

// cmd.Ordered
type Ordered interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 |
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
~float32 | ~float64 |
~string
}

func Less[T Ordered](x, y T) bool {
return (isNaN(x) && !isNaN(y)) || x < y
}

func isNaN[T Ordered](x T) bool {
return x != x
}

The algorithm used in slices.Sort is identical to the one in sort.Slice, and since it uses generics, no reflection occurs. Additionally, Ordered.Less is a predefined function used in slices.Sort, whereas slices.Sort accepts a function as an argument. Passing a function as an argument can introduce additional function call overhead, which may impact performance, especially when there are many comparison functions involved.

Conclusion

We have explored the three slice sorting functions provided by Go. In the past, only sort.Slice existed, which used Tim sort, though it wasn’t mentioned on this post. However, since 2022, Go has adopted pdqsort, a modern sorting algorithm that combines the fast average case performance of randomized quicksort with the efficient worst-case performance of heapsort, and achieves linear time for certain input patterns. This change improved performance, and subsequent versions, starting with Go 1.22, introduced slices.Sort and slices.SortFunc, further enhancing sorting performance.

Now, you can experience better sorting performance by using slices.Sort for sorting tasks, or slices.SortFunc when a custom comparison function is needed.

As Go continues to evolve, it is essential for us to keep an eye on these advancements.

--

--

No responses yet