I'm learning the Go and now I've reached the point of studying arrays and slices. I'm referring to Effective Go and A Tour of Go in this question. I have some doubts in the formulation of slice's capacity. See comments in the code below.
package main
import "fmt"
func main() {
// let's say I need to implement 5x5 picture with 2D slice
// and I have two ways to do that in acc. with
// https://go.dev/doc/effective_go#two_dimensional_slices
// that said:
// 1. six slices - six underlying arrays
picture := make([][]uint8, 5)
for i := range picture {
picture[i] = make([]uint8, 5)
}
// 2. six slices - two* underlying arrays
// *I THOUGHT the efficiency is hiding in number of arrays
picture = make([][]uint8, 5)
pixels := make([]uint8, 25)
for i := range picture {
// every iteration pixels[5:] allocates new array...
picture[i], pixels = pixels[:5], pixels[5:]
// ...but in the new iteration it's deleted
// leaving slice of initial pixels array in picture[i]...
}
// ...and here we have only two arrays...
// ... ^^^ B5t ^^^
// there are six arrays anyway
// but their capacity is 5 for picture and
// 25, 20...5 respectively for its contents
fmt.Print(cap(picture), " ")
for i := range picture {
fmt.Print(cap(picture[i]), " ")
}
fmt.Println()
// if you don't understand let me explain my position
// capacity is characteristic of underlying array rather than
// of slice itself
// in acc. with https://go.dev/tour/moretypes/11
// "The capacity of a slice is the number of
// elements in the underlying array..."
// I believe that capacity of array is immutable one and
// one array cannot have different cap's at the same time
picture = make([][]uint8, 5)
for i := range picture {
picture[i] = make([]uint8, 5)
}
// here we have six arrays but capacity of each is 5
// so the first solution works efficiently than second one
// I believe
fmt.Print(cap(picture), " ")
for i := range picture {
fmt.Print(cap(picture[i]), " ")
}
fmt.Println()
}
The output of this code is:
5 25 20 15 10 5
5 5 5 5 5 5
Can someone explain me where the efficiency of using one allocation method is hiding?
The second Effective Go Two-dimensional slices example was created before Full slice expressions were added to The Go Programming Language Specification.
one allocation, sliced into lines:
// Allocate the top-level slice, the same as before.
picture := make([][]uint8, YSize) // One row per unit of y.
// Allocate one large slice to hold all the pixels.
pixels := make([]uint8, XSize*YSize) // Has type []uint8 even though picture is [][]uint8.
// Loop over the rows, slicing each row from the front of the remaining pixels slice.
for i := range picture {
picture[i], pixels = pixels[:XSize], pixels[XSize:]
The second example should read:
one allocation, sliced into lines:
// Allocate the top-level slice, the same as before.
picture := make([][]uint8, YSize) // One row per unit of y.
// Allocate one large slice to hold all the pixels.
pixels := make([]uint8, XSize*YSize) // Has type []uint8 even though picture is [][]uint8.
// Loop over the rows, slicing each row from the front of the remaining pixels slice.
for i := range picture {
// Use a full slice expression for the correct row capacity
picture[i], pixels = pixels[:XSize:XSize], pixels[XSize:]
}
package main
import (
"fmt"
)
func main() {
XSize, YSize := 5, 5
// Effective Go: Two-dimensional slices
// https://go.dev/doc/effective_go#two_dimensional_slices
// one allocation, sliced into lines:
// Allocate the top-level slice, the same as before.
picture := make([][]uint8, YSize) // One row per unit of y.
// Allocate one large slice to hold all the pixels.
pixels := make([]uint8, XSize*YSize) // Has type []uint8 even though picture is [][]uint8.
// Loop over the rows, slicing each row from the front of the remaining pixels slice.
for i := range picture {
// Use a full slice expression for the correct row capacity
// https://go.dev/ref/spec#Slice_expressions
picture[i], pixels = pixels[:XSize:XSize], pixels[XSize:]
}
fmt.Print(cap(picture), " ")
for i := range picture {
fmt.Print(cap(picture[i]), " ")
}
fmt.Println()
}
https://go.dev/play/p/7I2ZL5Jj8Lw.
Output:
5 5 5 5 5 5
A simple Go benchmark illustrates some of the effects of minimizing allocations.
Benchmark1stExample-16 6710131 175.6 ns/op 154 B/op 6 allocs/op
Benchmark2ndExample-16 9674449 121.3 ns/op 160 B/op 2 allocs/op