What is considered "small" object in Go regarding stack allocation?
Asked Answered
S

2

3

The code:

func MaxSmallSize() {
    a := make([]int64, 8191)
    b := make([]int64, 8192)
    _ = a
    _ = b
}

Then run go build -gcflags='-m' . 2>&1 to check memory allocation details. The result:

./mem.go:10: can inline MaxSmallSize
./mem.go:12: make([]int64, 8192) escapes to heap
./mem.go:11: MaxSmallSize make([]int64, 8191) does not escape

My question is why a is small object and b is large object?

make 64KB will escape to heap and less will allocate in stack. Does the _MaxSmallSize = 32 << 10 is the reason?

go env

GOARCH="amd64"
GOBIN=""
GOEXE=""
GOHOSTARCH="amd64"
GOHOSTOS="linux"
GOOS="linux"
GOPATH="/vagrant/gopath"
GORACE=""
GOROOT="/home/vagrant/go"
GOTOOLDIR="/home/vagrant/go/pkg/tool/linux_amd64"
CC="gcc"
GOGCCFLAGS="-fPIC -m64 -pthread -fmessage-length=0 -fdebug-prefix-map=/tmp/go-build201775001=/tmp/go-build"
CXX="g++"
CGO_ENABLED="1"
Selfpollination answered 15/2, 2017 at 7:34 Comment(1)
This is implementation dependent, different compilers might do it differently, different architectures might do it differently and different releases may handle it differently. So what exactly is your question?Hydatid
L
3

Since this is not mentioned in the language spec, it is an implementation detail, and as such, it may vary based on a number of things (Go version, target OS, architecture etc.).

If you want to find out its current value or a place to start digging, check out the cmd/compile/internal/gc package.

The escape analysis which decides where to allocate the variable is in cmd/compile/internal/gc/esc.go. Check of the make slice operation is in unexported function esc():

func esc(e *EscState, n *Node, up *Node) {
    // ...

    // Big stuff escapes unconditionally
    // "Big" conditions that were scattered around in walk have been gathered here
    if n.Esc != EscHeap && n.Type != nil &&
        (n.Type.Width > MaxStackVarSize ||
            (n.Op == ONEW || n.Op == OPTRLIT) && n.Type.Elem().Width >= 1<<16 ||
            n.Op == OMAKESLICE && !isSmallMakeSlice(n)) {
        if Debug['m'] > 2 {
            Warnl(n.Lineno, "%v is too large for stack", n)
        }
        n.Esc = EscHeap
        addrescapes(n)
        escassignSinkNilWhy(e, n, n, "too large for stack") // TODO category: tooLarge
    }

    // ...
}

The decision involving the size is in function isSmallMakeSlice(), this is in file cmd/compile/internal/gc/walk.go:

func isSmallMakeSlice(n *Node) bool {
    if n.Op != OMAKESLICE {
        return false
    }
    l := n.Left
    r := n.Right
    if r == nil {
        r = l
    }
    t := n.Type

    return Smallintconst(l) && Smallintconst(r) && (t.Elem().Width == 0 || r.Int64() < (1<<16)/t.Elem().Width)
}

The size limit is this:

r.Int64() < (1<<16)/t.Elem().Width

r is the length or capacity of the slice (if cap is provided), t.Elem().Width is the byte size of the element type:

NumElem < 65536 / SizeElem

In your case:

NumElem < 65536 / 8 = 8192

So if the slice type is []uint64, 8192 is the limit from which it is allocated on the heap (instead of the stack), just as you experienced.

Liebfraumilch answered 15/2, 2017 at 8:25 Comment(0)
T
3

@icza's answer is really insightful, I'd just like to add that the link is a bit outdated 5 years later, you can find the code in cmd/compile/internal/escape/utils.go and in turn cmd/compile/internal/ir/cfg.go now:

// HeapAllocReason returns the reason the given Node must be heap
// allocated, or the empty string if it doesn't.
func HeapAllocReason(n ir.Node) string {
    // ... omitted for brevity
    if n.Op() == ir.OMAKESLICE {
        n := n.(*ir.MakeExpr)
        r := n.Cap
        if r == nil {
            r = n.Len
        }
        if !ir.IsSmallIntConst(r) {
            return "non-constant size"
        }
        if t := n.Type(); t.Elem().Size() != 0 && ir.Int64Val(r) > ir.MaxImplicitStackVarSize/t.Elem().Size() {
            return "too large for stack"
        }
    }

    return ""
}

and ir.MaxImplicitStackVarSize is:

package ir

var (
    // maximum size of implicit variables that we will allocate on the stack.
    //   p := new(T)          allocating T on the stack
    //   p := &T{}            allocating T on the stack
    //   s := make([]T, n)    allocating [n]T on the stack
    //   s := []byte("...")   allocating [n]byte on the stack
    // Note: the flag smallframes can update this value.
    MaxImplicitStackVarSize = int64(64 * 1024)
)
Triptych answered 3/10, 2022 at 14:1 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.