DEV Community

Cover image for [Go] Slice allocation
Julian-Chu
Julian-Chu

Posted on

[Go] Slice allocation

In previous article, we understand slice is a struct to operate an array. And array has a fixed-size, what happens if adding more element to an array which is full? The answer is that go will allocate a new array, and create new slice to point to it.

Let's write some code to test.

Append element to slice

s1 := make([]int, 2, 3)
s1[0] = 0
s1[1] = 1
fmt.Printf("s1:%v len=%d, cap=%d, addr to array:%v\n", s1, len(s1), cap(s1), &s1[0])
s2 := append(s1, 2)
fmt.Printf("s2:%v len=%d, cap=%d, addr to array:%v\n", s2, len(s2), cap(s2), &s2[0])
// append element to a full array
s3:= append(s2, 3)
fmt.Printf("s3:%v len=%d, cap=%d, addr to new array:%v\n", s3, len(s3), cap(s3), &s3[0])
fmt.Println("change element at index 1")
s1[1] = 99
fmt.Printf("s1:%v,\ns2:%v,\ns3:%v",s1,s2,s3)

-----------output----------
s1:[0 1] len=2, cap=3, addr to array:0x414020
s2:[0 1 2] len=3, cap=3, addr to array:0x414020
s3:[0 1 2 3] len=4, cap=8, addr to new array:0x45e020
change element at index 1
s1:[0 99],
s2:[0 99 2],
s3:[0 1 2 3]

Alt Text

when we create s2, the length is not over capacity, so the element is added to the array diretly. But when adding one more element to s2, the number of elements is over capcacity. Go allocates a new array to s3.

Remove element from slice

s1:= []int{0,1,2,3}
fmt.Printf("s1:%v len=%d, cap=%d, addr to array:%v\n", s1, len(s1), cap(s1), &s1[0])
// remove  element at 1
s2:= append(s1[0:1], s1[2:]...)
fmt.Printf("s2:%v len=%d, cap=%d, addr to array:%v\n", s2, len(s2), cap(s2), &s2[0])

-----------output----------
s1:[0 1 2 3] len=4, cap=4, addr to array:0x414020
s2:[0 2 3] len=3, cap=4, addr to array:0x414020

Removing element from slice doesn't change capacity of slice. No allocation happens.

Capacity is the key to allocate

link is how go runtime handle growing slice and strategy for new cap.

// growslice handles slice growth during append.
// It is passed the slice element type, the old slice, and the desired new minimum capacity,
// and it returns a new slice with at least that capacity, with the old data
// copied into it.
// The new slice's length is set to the old slice's length,
// NOT to the new requested capacity.
// This is for codegen convenience. The old slice's length is used immediately
// to calculate where to write new values during an append.
// TODO: When the old backend is gone, reconsider this decision.
// The SSA backend might prefer the new length or to return only ptr/cap and save stack space.
func growslice(et *_type, old slice, cap int) slice {
...
}

Understanding relationship between cap and allocation, then....?

Here I provide two examples, hope they are helpful.

  • Initializing slice with required capacity if you know the size. It will improve performance by avoiding unnecessary allocation.
// cap = 0
func sliceWithZeroCap(){
    res:=make([]int,0,0)
    for i:=0; i <1000;i++{
        res = append(res, i)
    }
}

//cap = 1000
func sliceWithAssignedCap(){
    res:=make([]int,0,1000)
    for i:=0; i <1000;i++{
        res = append(res, i)
    }
}

//cap = 1000,  slice is initialized by zeros
func sliceWithAssignedCapAndInit(){
    res:=make([]int,1000,1000)
    for i:=0; i <1000;i++{
        res[i]=i
    }
}


------benchmark results------
$ go test -v -bench=. -benchmem
goos: windows
goarch: amd64
pkg: playground/slice
Benchmark_sliceWithZeroCap-8               300000    4633 ns/op    16376 B/op  11 allocs/op
Benchmark_sliceWithAssignedCap-8          2000000     759 ns/op        0 B/op   0 allocs/op
Benchmark_sliceWithAssignedLenAndCap-8    5000000     411 ns/op        0 B/op   0 allocs/op

  • Forcing allocation to avoid modifying the same array issue
s := []int{0, 1, 2}
// s1 still point to s 
// using s[0:2:2], if you want new slice and array
s1 := append(s[0:2], 3)
// expect s2 = [0,1,3], but s1 and s point to the same array
s2 := append(s[0:2:2], s[len(s)-1]+1)

fmt.Printf("s:%v len=%d, cap=%d, addr to array:%v\n", s, len(s), cap(s), &s[0])
fmt.Printf("s1:%v len=%d, cap=%d, addr to array:%v\n", s1, len(s1), cap(s1), &s1[0])
fmt.Printf("s2:%v len=%d, cap=%d, addr to array:%v\n", s2, len(s2), cap(s2), &s2[0])


-----------output----------
s:[0 1 3] len=3, cap=3, addr to array:0xc000068140
s1:[0 1 3] len=3, cap=3, addr to array:0xc000068140
s2:[0 1 4] len=3, cap=4, addr to array:0xc000068160

Understanding the cap and using it properly can avoid some mistakes and improve performance.

Top comments (0)