Mihail Ivanov

Benchmarking Go: two ways of comparing slices

image of pineapples

I read in a yourbasic’s Go go-to guide article there are different approaches for comparing slices in Go. First one is writing a function that loops through the elements of each slice and compares them. Second approach is to use reflect.DeepEqual.

The article states the second approach is slower. I’m curious to find out how much slower it is by using benchmark testing.

First - the code.

Approach one

The function will return false immediately if the lengths of the slices is different. Otherwise, it will compare the elements of both slices to see if the slices are equal.

// Equal tells whether a and b contain the same elements.
// A nil argument is equivalent to an empty slice.
func Equal(a, b []int) bool {
	if len(a) != len(b) {
		return false
	}
	for i, v := range a {
		if v != b[i] {
			return false
		}
	}
	return true
}

Approach two

By using reflect.DeepEqual the function is much smaller.

func EqualWithReflect(a, b []int) bool {
	return reflect.DeepEqual(a, b)
}

Benchmarking

I’m going to use Benchmark functions from the testing package to compare the performance of the two approaches.

Case 1: Equal slices with 5 elements each

func BenchmarkEqual4(b *testing.B) {
	x := []int{1, 2, 3, 4, 5}
	y := []int{1, 2, 3, 4, 5}

	Equal(x, y)
}

func BenchmarkEqualWithReflect4(b *testing.B) {
	x := []int{1, 2, 3, 4, 5}
	y := []int{1, 2, 3, 4, 5}

	EqualWithReflect(x, y)
}

Case 2: Equal slices with 10,000,000 elements

func BenchmarkEqual10000000(b *testing.B) {
	x := make([]int, 10000000)
	y := make([]int, 10000000)

	Equal(x, y)
}

func BenchmarkEqualWithReflect10000000(b *testing.B) {
	x := make([]int, 10000000)
	y := make([]int, 10000000)

	EqualWithReflect(x, y)
}

Case 3: Different slices with 10,000,000 elements

func BenchmarkEqualDifferentElements(b *testing.B) {
	x := make([]int, 10000000)
	y := make([]int, 10000000)

	x[2] = 1

	Equal(x, y)
}

func BenchmarkEqualWithReflectDifferentElements(b *testing.B) {
	x := make([]int, 10000000)
	y := make([]int, 10000000)

	x[2] = 1

	EqualWithReflect(x, y)
}

Case 4: Different slices of different size

func BenchmarkEqualDifferentLength(b *testing.B) {
	x := make([]int, 10000000)
	y := make([]int, 20000000)

	Equal(x, y)
}

func BenchmarkEqualWithReflectDifferentLength(b *testing.B) {
	x := make([]int, 10000000)
	y := make([]int, 20000000)

	EqualWithReflect(x, y)
}

And the results

$ go test -bench=.
goos: linux
goarch: amd64
pkg: github.com/mihail-i4v/benchmark-go
cpu: AMD Ryzen 7 PRO 6850U with Radeon Graphics     
BenchmarkEqual4-16                               	1000000000	         0.0000019 ns/op
BenchmarkEqualWithReflect4-16                    	1000000000	         0.0000052 ns/op
BenchmarkEqual10000000-16                        	1000000000	         0.05434 ns/op
BenchmarkEqualWithReflect10000000-16             	1000000000	         0.1868 ns/op
BenchmarkEqualDifferentElements-16               	1000000000	         0.02851 ns/op
BenchmarkEqualWithReflectDifferentElements-16    	1000000000	         0.02779 ns/op
BenchmarkEqualDifferentLength-16                 	1000000000	         0.03784 ns/op
BenchmarkEqualWithReflectDifferentLength-16      	1000000000	         0.03388 ns/op
PASS
ok  	github.com/mihail-i4v/benchmark-go	3.945s

In cases 1 and 2, the reflect.DeepEqual approach is nearly 3 times slower when used with smaller slices. However, with bigger slices (cases 3 and 4), the reflect.DeepEqual approach has similar performance if not slightly better.

Having an array with 10 million elements is fairly unrealistic scenario. In the majority of cases, the first approach will be the better option in terms of performance.

You can find all the code in https://github.com/mihail-i4v/benchmark-go.