Maksat Yalkabov
Maksat Yalkabov

Reputation: 558

Capacity of slices in Go

I have a question about the below code

package main

import "fmt"

func main() {
    var a []int
    printSlice("a", a)

    // append works on nil slices.
    a = append(a, 0)
    printSlice("a", a)

    // the slice grows as needed.
    a = append(a, 1)
    printSlice("a", a)

    // we can add more than one element at a time.
    a = append(a, 2, 3, 4)
    printSlice("a", a)
}

func printSlice(s string, x []int) {
    fmt.Printf("%s len=%d cap=%d %v\n",
    s, len(x), cap(x), x)
}       

I always guess what the result of running a piece of code will look like then run the code and check if my guess is correct. But this code resulted a little bit different from my guess:

Result:
On my local go tour server:

a len=0 cap=0 []
a len=1 cap=1 [0]
a len=2 cap=2 [0 1]
a len=5 cap=6 [0 1 2 3 4]      

Everything is ok until the last line but I don't get

cap=6     

why not

cap=5    

My opinion is I did not create slice with explicit capacity therefore my system gave it this value of 6.

2) But when I tried this same code on the golang tour server I get a little more diffferent result like this :

a len=0 cap=0 []
a len=1 cap=2 [0]
a len=2 cap=2 [0 1]
a len=5 cap=8 [0 1 2 3 4]   

What about cap=2 on the second line and cap=8 on the last line?

Upvotes: 14

Views: 5931

Answers (2)

secret42
secret42

Reputation: 21

In fact, after the cap capacity is calculated, a roundupsize() function will be run, which will calculate the real cap due to memory alignment considerations.

newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
    newcap = cap
} else {
    if old.cap < 1024 {
        newcap = doublecap
    } else {
        for 0 < newcap && newcap < cap {
            newcap += newcap / 4
        }
        if newcap <= 0 {
            newcap = cap
        }
    }
}
......
capmem = roundupsize(uintptr(newcap) * sys.PtrSize)
newcap = int(capmem / sys.PtrSize)

In this example, the size after the first expansion is expected to be sizeof(int64)*5=40, and then the first value greater than or equal to this number will be found in the array below, here is 48, so the true cap is 48 /sizeof(int64)=6

class_to_size = uint16{0, 8, 16, 24, 32, 48, 64, 80, 96,112, 128, 144, 160, 176, 192, 208, 224,...

As for why the results of the two runs are different, I guess it should be related to the operating system. For more details, see https://github.com/golang/go/blob/master/src/runtime/slice.go#L164 https://github.com/golang/go/blob/master/src/runtime/msize.go

Upvotes: 2

user1087001
user1087001

Reputation:

This question isn't an exact duplicate, but my answer here also effectively answers this.

TL;DR — how much the capacity of the slice is expanded by isn't mentioned in the specification and different versions of Go (or different implementations, or the same version on different architectures, etc.) can expand the slice by different amounts.

The reason you might want to make the capacity larger than you need is because underneath the slice there is an array which is immutable (it can't be expanded). When you "grow" a slice what actually happens is that you make a new (longer) array, copy all the values over, and then set that as the backing array for the slice. If you were appending lots of values, you'd have to do lots and lots of copies (one for every single value), which would be very slow, so instead the runtime allocates more space than it thinks you need so that it has to make copies less often.

Upvotes: 17

Related Questions