I want to be on the stack. No, you should be on the pile

  Back end, golang, php, Stack context

image

Original address:I want to be on the stack. No, you should be on the pile

Preface

When we write code, we sometimes wonder where this variable has been allocated. At this time, some people may say, on the stack, on the heap. Believe me, it must be true …

However, from the results, you still know a little about it, which is not acceptable, in case you are confused. Today, let’s dig deep into the secrets of Go here and make our own money.

Problem

type User struct {
    ID     int64
    Name   string
    Avatar string
}

func GetUserInfo() *User {
    return &User{ID: 13746731, Name: "EDDYCJY", Avatar: "https://avatars0.githubusercontent.com/u/13746731"}
}

func main() {
    _ = GetUserInfo()
}

The beginning is a question mark, learning with problems. Main call, pleaseGetUserInfoReturning after&User{...}. Is this variable allocated to the stack or to the heap?

What is heap/stack

It is not intended to introduce the stack in detail here, but only the basic knowledge required in this article. As follows:

  • Heap: Generally speaking, it is managed manually by human beings, applying, distributing and releasing manually. In general, the memory size involved is not constant, and larger objects are usually stored. In addition, its allocation is relatively slow and involves relatively many instruction actions.
  • Stack: managed by the compiler and automatically applied, allocated and released. Generally not too big, our common function parameters (different platforms allow different numbers of storage), local variables and so on will be stored on the stack

The Go language we introduced today, its stack allocation is analyzed by Compiler and managed by GC, and its analysis and selection actions are the focus of today’s discussion.

What is Escape Analysis

In compiler optimization theory, escape analysis is a method to determine the dynamic range of the pointer, which is simply to analyze where the pointer can be accessed in the program.

Generally speaking, escape analysis is to determine whether a variable should be put on the heap or on the stack. The rules are as follows:

  1. Whether it has been quoted elsewhere (not locally). As long asPossibleIs quoted, then itCertainAssigned to the heap. Otherwise, it is allocated to the stack.
  2. Even if it is not externally referenced, the object is too large to be stored on the stack area. It is still possible to allocate it to the heap.

For this, you can understand that escape analysis is a behavior used by the compiler to determine whether variables are allocated to the heap or stack.

At what stage will escape be established

Escape is established at compile time, noting that it is not at runtime.

Why do you need to escape

We can think about this problem in reverse, what will happen if all the variables are assigned to the heap? For example:

  • The pressure of garbage collection (GC) is constantly increasing.
  • The system overhead of applying, allocating and reclaiming memory increases (relative to stack)
  • Dynamic allocation generates a certain amount of memory fragments

In fact, in general, there is a “price” for frequent application and allocation of heap memory. It will affect the efficiency of application running and indirectly affect the overall system. Therefore, “distribution according to needs” to maximize the flexible use of resources is the correct way of governance. This is why escape analysis is needed, do you think?

How to determine whether to escape

First, through compiler commands, you can see the detailed escape analysis process. The instruction set-gcflagsUsed to pass identification parameters to Go compiler, involving the following:

  • -mThe optimization strategy of escape analysis will be printed out. In fact, a total of up to 4 can be used.-mHowever, the amount of information is relatively large, generally one is enough.
  • -lFunction inlining will be disabled. disabling inline here can better observe escape and reduce interference.
$ go build -gcflags '-m -l' main.go

Second, look through the decompilation command

$ go tool compile -S main.go

Note: It can be passedgo tool compile -helpView all identification parameters that are allowed to be passed to the compiler

Escape case

Case 1: Pointer

The first case is the question raised at the beginning. Now look at it again and think about it, as follows:

type User struct {
    ID     int64
    Name   string
    Avatar string
}

func GetUserInfo() *User {
    return &User{ID: 13746731, Name: "EDDYCJY", Avatar: "https://avatars0.githubusercontent.com/u/13746731"}
}

func main() {
    _ = GetUserInfo()
}

Execute the command and observe, as follows:

$ go build -gcflags '-m -l' main.go
# command-line-arguments
./main.go:10:54: &User literal escapes to heap

By looking at the analysis results, we can know that&UserEscape to the heap, that is, assigned to the heap. This is not a problem ah … look at the assembly code to determine, as follows:

$ go tool compile -S main.go                
"".GetUserInfo STEXT size=190 args=0x8 locals=0x18
    0x0000 00000 (main.go:9)    TEXT    "".GetUserInfo(SB), $24-8
    ...
    0x0028 00040 (main.go:10)    MOVQ    AX, (SP)
    0x002c 00044 (main.go:10)    CALL    runtime.newobject(SB)
    0x0031 00049 (main.go:10)    PCDATA    $2, $1
    0x0031 00049 (main.go:10)    MOVQ    8(SP), AX
    0x0036 00054 (main.go:10)    MOVQ    $13746731, (AX)
    0x003d 00061 (main.go:10)    MOVQ    $7, 16(AX)
    0x0045 00069 (main.go:10)    PCDATA    $2, $-2
    0x0045 00069 (main.go:10)    PCDATA    $0, $-2
    0x0045 00069 (main.go:10)    CMPL    runtime.writeBarrier(SB), $0
    0x004c 00076 (main.go:10)    JNE    156
    0x004e 00078 (main.go:10)    LEAQ    go.string."EDDYCJY"(SB), CX
    ...

We focused our attention on the CALL instruction and found that it was executed.runtime.newobjectMethod, that is, is indeed allocated to the heap. Why is this?

Analysis results

This is becauseGetUserInfo()The pointer object is returned, and the reference is returned outside the method. Therefore, the compiler will allocate the object to the heap instead of the stack. Otherwise, after the method is finished, the local variables will be recovered, which is not a rollover. Therefore, the final allocation to the heap is taken for granted.

Think again

Then you might think, is that all pointer objects should be on the heap? Not really. As follows:

func main() {
    str := new(string)
    *str = "EDDYCJY"
}

Where do you think this object will be allocated? As follows:

$ go build -gcflags '-m -l' main.go
# command-line-arguments
./main.go:4:12: main new(string) does not escape

Obviously, the object is allocated to the stack. The core point is whether it has been referenced outside the scope, and here the scope remainsmainTherefore, it did not escape

Case 2: Uncertain Type

func main() {
    str := new(string)
    *str = "EDDYCJY"

    fmt.Println(str)
}

Execute the command and observe, as follows:

$ go build -gcflags '-m -l' main.go
# command-line-arguments
./main.go:9:13: str escapes to heap
./main.go:6:12: new(string) escapes to heap
./main.go:9:13: main ... argument does not escape

By looking at the analysis results, we can know thatstrThe variable escaped to the heap, that is, the object was allocated on the heap. But in the last case it was still on the stack, and wefmtI just exported it. What the hell is going on here?

Analysis results

Compared with case one, case two has only one line of code added.fmt.Println(str), the problem must be on it. Its prototype:

func Println(a ...interface{}) (n int, err error)

By analyzing it, we can know that when the formal parameter isinterfaceType, the compiler cannot determine its specific type at compile time. Therefore, escape will occur and eventually be distributed to the heap.

If you are interested in pursuing the source code, you can look at the internal ones.reflect.TypeOf(arg).Kind()Statement, which causes the heap to escape, and the appearance isinterfaceType causes the object to be assigned to the heap

Case 3: Leakage Parameters

type User struct {
    ID     int64
    Name   string
    Avatar string
}

func GetUserInfo(u *User) *User {
    return u
}

func main() {
    _ = GetUserInfo(&User{ID: 13746731, Name: "EDDYCJY", Avatar: "https://avatars0.githubusercontent.com/u/13746731"})
}

Execute the command and observe, as follows:

$ go build -gcflags '-m -l' main.go
# command-line-arguments
./main.go:9:18: leaking param: u to result ~r1 level=0
./main.go:14:63: main &User literal does not escape

We have noticed thatleaking paramThe expression of, it illustrates the variableuIs a leak parameter. Combined with the code, it can be known that it is passed toGetUserInfoAfter the method, the variable was returned directly without any reference or other actions involving the variable. So this variable did not actually escape, its scope is still theremain(), so it is allocated on the stack.

Think again

Then how can you allocate it to the heap? In combination with case one, draw an analogy from one another. Amend to read as follows:

type User struct {
    ID     int64
    Name   string
    Avatar string
}

func GetUserInfo(u User) *User {
    return &u
}

func main() {
    _ = GetUserInfo(User{ID: 13746731, Name: "EDDYCJY", Avatar: "https://avatars0.githubusercontent.com/u/13746731"})
}

Execute the command and observe, as follows:

$ go build -gcflags '-m -l' main.go
# command-line-arguments
./main.go:10:9: &u escapes to heap
./main.go:9:18: moved to heap: u

As long as a small change is made, it will be considered to be externally referenced, so it is properly allocated to the heap.

Summary

In this article, I introduce the concept and rules of escape analysis to you, and give some examples to deepen your understanding. However, the actual situation is definitely far more than these cases. What you need to do is to master the methods and look at them again when you meet them. In addition, you also need to pay attention to:

  • Static allocation to the stack must have better performance than dynamic allocation to the heap.
  • Is the bottom layer allocated to the heap or the stack? In fact, it is transparent to you and you don’t need to care too much about it.
  • Escape analysis will be different for each Go version (will change, will be optimized)
  • Directly throughgo build -gcflags '-m -l'You can see the process and results of escape analysis.
  • It is not necessarily the best to use pointers everywhere, but to use them correctly.

Before, I had thought about whether to write articles related to “escape analysis”. Until recently, I saw someone asking me in the night reading, it was still necessary to write. For this piece of knowledge. My suggestion is to understand it properly, but there is no need to memorize it. Just rely on basic knowledge points and commands to debug and observe. As Cao da said before, “you think about half a day of escape analysis, a pressure test, the bottleneck is locked”, there is no need to pay too much attention …

References