Have you ever wonder when to use *[]struct
and when to use []*struct
when developing in Go? Look no further! I’ll explain everything you need to know about this topic in this post.
Conclusion
If the underlying array of your slice is big enough for your need, there is no point at using slice of pointers, as it will need to allocate the a new piece of memory for the pointer for each entry in the slice.
However, if the your your slice will outgrow the underlying array when being appended, a new array with sufficient length will be allocated, copying all the data from the original array to the new one. This is when using slice of pointers is a better option, since only the pointers will need to be copied, not the entire values.
I don’t believe it
To test the performance difference yourself:
main_test.go
package main
import (
"testing"
)
type SmallStruct struct {
A int
B int
}
const (
SLICE_LEN = 100
ARRAY_LEN = 100
)
func BenchmarkSliceOfSmallStructs(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
slice := make([]SmallStruct, 0, ARRAY_LEN)
for j := 0; j < SLICE_LEN; j++ {
slice = append(slice, SmallStruct{A: j, B: j + 1})
}
}
}
func BenchmarkSliceOfPointersOfSmallStructs(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ { // test count
slice := make([]*SmallStruct, 0, ARRAY_LEN)
for j := 0; j < SLICE_LEN; j++ {
slice = append(slice, &SmallStruct{A: j, B: j + 1})
}
}
}
type BigStruct struct {
F1, F2, F3, F4, F5, F6, F7 string
I1, I2 int
I3, I4, I5, I6, I7, I8, I9, I10, I11, I12 int
A1, A2, A3, A4, A5, A6, A7 SmallStruct
A8, A9, A10, A11, A12, A13, A14 SmallStruct
}
func BenchmarkSliceOfBigStructs(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
slice := make([]BigStruct, 0, ARRAY_LEN)
for j := 0; j < SLICE_LEN; j++ {
slice = append(slice, BigStruct{
I1: j,
I2: j + 1,
})
}
}
}
func BenchmarkSliceOfPointersOfBigStructs(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
slice := make([]*BigStruct, 0, ARRAY_LEN)
for j := 0; j < SLICE_LEN; j++ {
slice = append(slice, &BigStruct{
I1: j,
I2: j + 1,
})
}
}
}
To see the direct outputs
go test -bench . -count 3
To see analyzed outputs, install benchstat
first
go install golang.org/x/perf/cmd/benchstat@latest
and then
go test -bench . -count 10 -benchmem >> benchmark.txt
benchstat benchmark.txt
(You don’t need to add the -benchmen
flag if you already have b.ReportAllocs()
in your test functions actually.)
Output when SLICE_LEN
= 100, ARRAY_LEN
= 100:
goos: darwin
goarch: amd64
pkg: gslice
cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
│ benchmark.txt │
│ sec/op │
SliceOfSmallStructs-12 89.89n ± 4%
SliceOfPointersOfSmallStructs-12 2.133µ ± 8%
SliceOfBigStructs-12 3.363µ ± 7%
SliceOfPointersOfBigStructs-12 8.597µ ± 3%
geomean 1.534µ
│ benchmark.txt │
│ B/op │
SliceOfSmallStructs-12 0.000 ± 0%
SliceOfPointersOfSmallStructs-12 1.562Ki ± 0%
SliceOfBigStructs-12 0.000 ± 0%
SliceOfPointersOfBigStructs-12 43.75Ki ± 0%
geomean ¹
¹ summaries must be >0 to compute geomean
│ benchmark.txt │
│ allocs/op │
SliceOfSmallStructs-12 0.000 ± 0%
SliceOfPointersOfSmallStructs-12 100.0 ± 0%
SliceOfBigStructs-12 0.000 ± 0%
SliceOfPointersOfBigStructs-12 100.0 ± 0%
geomean ¹
¹ summaries must be >0 to compute geomean
We can see that slice of structs is better than slice of pointers in every aspect, by a mile.
Output when SLICE_LEN
= 100, ARRAY_LEN
= 10:
benchstat benchmark_raw.txt > benchmark.txt
cat benchmark.txt
goos: darwin
goarch: amd64
pkg: gslice
cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
│ benchmark_raw.txt │
│ sec/op │
SliceOfSmallStructs-12 880.6n ± 17%
SliceOfPointersOfSmallStructs-12 2.815µ ± 7%
SliceOfBigStructs-12 20.41µ ± 15%
SliceOfPointersOfBigStructs-12 9.213µ ± 3%
geomean 4.646µ
│ benchmark_raw.txt │
│ B/op │
SliceOfSmallStructs-12 4.812Ki ± 0%
SliceOfPointersOfSmallStructs-12 3.906Ki ± 0%
SliceOfBigStructs-12 147.2Ki ± 0%
SliceOfPointersOfBigStructs-12 46.09Ki ± 0%
geomean 18.90Ki
│ benchmark_raw.txt │
│ allocs/op │
SliceOfSmallStructs-12 4.000 ± 0%
SliceOfPointersOfSmallStructs-12 104.0 ± 0%
SliceOfBigStructs-12 4.000 ± 0%
SliceOfPointersOfBigStructs-12 104.0 ± 0%
geomean 20.40
We can see that slice of pointers use less time and memory then slice of structs when it has to constantly reallocating new arrays and copying old entries, especially when each entry is a big struct.
References
Read Arrays, slices (and strings): The mechanics of ‘append’ | Go Blog. You’ll understand everything after reading this official blog post thoroughly. You can also read my note about slice in Go.
Some discussions containing partial truths: