foreword

This is part 3 of a series on the top 10 most common mistakes in Go: performance issues and memory escapes with Go pointers. The material comes from Teiva Harsanyi , a Go evangelist and a senior engineer at Docker [1] .

The source code involved in this article is all open source in: Source code of top ten common errors in Go [2] , you are welcome to pay attention to the public account and get the latest updates of this series in time.

Scenes

We know that function parameters and return values ​​can use variables or pointers to variables.

Go beginners are prone to a misunderstanding:

  • It is believed that if the function parameters and return values ​​use the value of the variable, the entire variable will be copied, which is slow
  • Think that if the function parameters and return values ​​use pointer types, only the memory address needs to be copied, which is faster

But is this really the case? We can look at this example [3] of this code, and the results of the performance test are as follows:

$ go test -bench .
goos: darwin
goarch: amd64
pkg: pointer
cpu: Intel(R) Core(TM) i5-5250U CPU @ 1.60GHz
BenchmarkByPointer-4     6473781               178.2 ns/op
BenchmarkByValue-4      21760696                47.11 ns/op
PASS
ok      pointer 2.894s

It can be seen that the function that uses pointers for both parameters and return values ​​is much slower than the function that uses variable values ​​for both parameters and return values. The time-consuming of the former is 4 times that of the latter.

Beginners may feel a little counter-intuitive when they see this, why is this happening?

This is related to Go's memory management of stack (stack) and heap (heap), and whether variables are allocated on stack or heap will have an impact on performance.

  • The efficiency of allocating memory on the stack is higher than that of the heap, and the memory allocated on the stack does not need to be GC, and the memory is automatically reclaimed when it exceeds the scope.

  • The memory placed on the heap needs to be reclaimed by the GC, and it is prone to memory fragmentation.

  • The compiler decides whether the variable is allocated on the stack or the heap at compile time. Escape analysis is required. The escape analysis is completed at the compile time.

What is escape analysis?

The Go compiler parses the source code and decides which variables are allocated in the stack memory space and which variables are allocated in the heap memory space. This process is called escape analysis, which is an analysis link in the Go code compilation process.

Through escape analysis, the compiler will try to allocate objects that can be allocated on the stack on the stack to avoid the system overhead caused by frequent GC garbage collection of heap memory and affect program performance (GC occurs only in heap memory space).

Case 1

Let's look at the following code: the structure foocan refer to this example [4] .

func getFooValue() foo {
 var result foo
 // Do something
 return result
}

The memory space resultallocated on the stack of this goroutine when the variable is defined.result

When the function returns, getFooValueif the caller receives the return value, that resultvalue will be copied to the corresponding receiving variable.

The memory space for variables on the stack resultwill be freed (marked as unavailable and can no longer be accessed unless the space is allocated to other variables again).

Notefoo : The memory space occupied by the structure in this case is relatively small, about 0.3KB, and the stack space of the goroutine is sufficient for storage. If foothe space occupied is too large to be stored in the stack, the memory will be allocated to the heap.

Case 2

Let's look at the following code:

func getFooPointer() *foo {
 var result foo
 // Do something
 return &result
}

Because the function getFooPointerreturns a pointer, if the variable is resultallocated on the stack, resultthe memory space will be released after the function returns, which will cause the variable receiving the return value of the function to be unable to access resultthe original memory space and become a dangling pointer. ).

So in this case, memory escape will occur, and resultit will be allocated on the heap, not the stack.

Case 3

Let's look at the following code:

func main()  {
 p := &foo{}
 f(p)
}

The pointer variable pis fthe actual parameter of the function, because we call the function in the goroutine where the main is located f, and there is no cross-goroutine, so the pointer variable pcan be allocated on the stack, and does not need to be allocated on the heap.

Summarize

So how do we know whether the variable is allocated on the stack or the head?

The official statement from Go is:

  • From a program correctness point of view, you don't need to care whether the variable is allocated on the stack or the heap. The memory space in which the variable is allocated does not change the semantics of the Go language.
  • From the perspective of program performance, you can care about whether the variable is allocated on the stack or the heap, because as mentioned above, the location of the variable storage has an impact on performance.

How do I know whether a variable is allocated on the heap or the stack?

From a correctness standpoint, you don't need to know. Each variable in Go exists as long as there are references to it. The storage location chosen by the implementation is irrelevant to the semantics of the language.

The storage location does have an effect on writing efficient programs. When possible, the Go compilers will allocate variables that are local to a function in that function's stack frame. However, if the compiler cannot prove that the variable is not referenced after the function returns , then the compiler must allocate the variable on the garbage-collected heap to avoid dangling pointer errors. Also, if a local variable is very large, it might make more sense to store it on the heap rather than the stack.

In the current compilers, if a variable has its address taken, that variable is a candidate for allocation on the heap. However, a basic escape analysis recognizes some cases when such variables will not live past the return from the function and can reside on the stack.

In general, escape behavior occurs in the following situations, and the Go compiler will store variables on the heap

  • Local variables within a function are referenced outside the function
  • Variables of type interface
  • Variables with unknown size or dynamic changes, such as slice, map, channel, []byte, etc.
  • Local variables whose size is too large, because the stack memory space is relatively small.

In addition, we can also help us with the help of memory escape analysis tools.

Because memory escape analysis is done by the compiler at compile time, you can use the following command to do memory escape analysis:

  • go build -gcflags="-m", which can display various optimization results such as escape analysis and inline optimization.
  • go build -gcflags="-m -l", -lwhich disables inline optimization, which filters out inline optimization results and allows us to focus on escape analysis results.
  • go build -gcflags="-m -m", one more -mwill show more detailed analysis results.

Recommended reading

open source address

Articles and sample code are open sourced on GitHub: Beginner, Intermediate and Advanced Tutorials in Go [8] .

Official account: coding advanced. Follow the official account to get the latest Go interview questions and technology stacks.

Personal website: Jincheng's Blog [9] .

Zhihu: Wuji [10] .

Welfare

I have compiled a back-end development learning material package for you, including programming language entry to advanced knowledge (Go, C++, Python), back-end development technology stack, interview questions, etc.

Follow the official account "coding advanced", send a message backend to receive a gift package, this information will be updated from time to time, and add information that I think is valuable. You can also send a message " join the group " to communicate and learn with your peers and answer questions.

References

[1]

Teiva Harsanyi: https://teivah.medium.com/

[2]

The source code of the top ten common errors in Go: https://github.com/jincheng9/go-tutorial/tree/main/workspace/senior/p28

[3]

this example: https://github.com/jincheng9/go-tutorial/blob/main/workspace/senior/p28/pointer/pointer_test.go

[4]

this example: https://github.com/jincheng9/go-tutorial/blob/main/workspace/senior/p28/pointer/pointer_test.go

[5]

Go stacks and pointers grammar mechanism: https://www.ardanlabs.com/blog/2017/05/language-mechanics-on-stacks-and-pointers.html

[6]

Escape Analysis Principles by ArdanLabs: https://www.ardanlabs.com/blog/2017/05/language-mechanics-on-escape-analysis.html

[7]

Escape Analysis Principles by Gopher Con: https://www.youtube.com/watch?v=ZMZpH4yT7M0

[8]

Go language beginner, intermediate and advanced tutorial: https://github.com/jincheng9/go-tutorial

[9]

Jincheng's Blog: https://jincheng9.github.io/

[10]

Wuji: https://www.zhihu.com/people/thucuhkwuji