当前位置:网站首页>Technical dry goods | understand go memory allocation

Technical dry goods | understand go memory allocation

2022-06-24 05:24:00 Tencent cloud Ti platform

author :rocketwang

If you're also a Go developer , Do you care about memory allocation and recycling ? The created object needs to be created by GC To recycle , Or as the call stack is ejected , Just disappeared ? GC As a result of Stop The World Does it cause the performance jitter of your program ?

This article briefly introduces Go Memory allocation logic , It introduces Go Memory partition model . And take the code as an example , Briefly introduce several scenarios Go Memory allocation logic . Subsequent use go build The command verifies the memory escape and other related verifications . Last , Summed up the memory allocation principles .

Need to care about memory allocation


Every engineer's time is so precious , Before continuing to read this article , I need you to answer a few questions first , If the answer is no , Maybe the content in this article doesn't help you . however , If you encounter performance problems caused by memory allocation , Maybe this article can help you understand Golang The tip of the iceberg of memory allocation , Take you through the door .

Questions as follows :

  • Is your program performance sensitive ?
  • GC Does the delay affect the performance of your program ?
  • Does your program have excessive heap memory allocation ?

If you hit one or both of the above questions , This article is suitable for you to continue reading . Or you don't know how to answer these questions , Maybe go and find out go Knowledge related to performance observation (pprof Use, etc. ) More helpful to you .

The following text begins .

Golang Brief memory partition


We can simply think that Golang When the program starts , A certain area of memory will be requested from the operating system , Divided into stacks (Stack) And heaps (Heap). Stack memory will be allocated and recycled as the function is called ; Heap memory is allocated by a program request , By the garbage collector (Garbage Collector) Responsible for recycling . On the performance , Stack memory is used and recycled more quickly ; Even though Golang Of GC Very efficient , But it will inevitably bring some performance losses . therefore ,Go Give priority to using stack memory for memory allocation . When you have to allocate objects to the heap , To put a specific object on the heap .

Memory allocation process analysis


This part , Will be in the form of code , Introduce stack memory allocation respectively 、 Stack memory allocation with pointer as parameter 、 The stack memory allocation in the case of pointer as return value gradually leads to escape analysis and several basic principles of memory escape .

Text begins ,Talk is cheap,show me the code.

Stack memory allocation

I will use a simple code as an example , Analyze the memory allocation process of this code .

package main
import "fmt"
func main() {  n := 4  n2 := square(n)  fmt.Println(n2)}
func square(n int) int{  return n * n}

The function of the code is very simple , One main Function as program entry , Defines a variable n, Defines another function squire , Returns the result of the power operation int value . Last , Print the returned value to the console . The program output is 16.

Let's start the analysis line by line , When parsing the call ,go How the runtime allocates memory .

When the code runs to the 6 That's ok , Get into main Function time , Will create a on the stack Stack frame, Store the variable information in this function . Include function name , Variable etc. .

When the code runs to the 7 Line time ,go A new one will be pushed into the stack Stack Frame, Used to store calls square Function information ; Include function name 、 Variable n And so on . here , Calculation 4 * 4 Value , And back to .

When square Function call completed , return 16 To main After the function , take 16 Assign a value to n2 Variable . Be careful , The original stack frame It's not going to be go Clean up , Instead, as the arrow on the left side of the stack shows , Marked as illegal . The horizontal line between the red arrow and the green arrow in the above figure can be understood as go In assembly code SP The value of the stack register , When a program requests or frees stack memory , It just needs to be modified SP Register value , This stack memory allocation method saves the time of cleaning up the stack memory space 【1】.

Next , call fmt.Println when ,SP The value of the register will increase further , Cover up the original square Functional stack frame, complete print after , Program exit normally .

Stack memory allocation with pointer as parameter

The same process , Look at the following code .

package main
import "fmt"
func main() {  n := 4  increase(&n)  fmt.Println(n)}
func increase(i *int) {  *i++}

main As program entry , Declared a variable n, The assignment is 4. Declared a function    increase, Use one int Pointer to type i As a parameter ,increase Within the function , To the pointer i The corresponding value is automatically increased . Last main The function prints n Value . The program output is 5.

When the program runs to main The function of the first 6 Line time ,go A... Is allocated on the stack stack frame , The variable n It's assigned ,n The corresponding address in memory is 0xc0008771, At this point, the program will continue to execute downward , call increase function .

At this time ,increase Function corresponding stack fream Be created ,i Is assigned as a variable n Corresponding address value 0xc0008771, Then carry out the auto increment operation .

When increase When the function is finished ,SP The register will move up , Put the previously assigned stack freme Marked as illegal . here , The program runs normally , Not because SP Register changes affect the correctness of the program , The values in memory are also modified correctly .

Stack memory allocation with pointer as return value

The previous part of the article introduced the stack memory usage when ordinary variables are used as parameters and pointers are used as parameters , This section describes how to use a pointer as a return value , Return to the caller , How memory is allocated , And introduce the related contents of memory escape . Look at this code :

package main
import "fmt"
func main() {  n := initValue()  fmt.Println(*n/2)}
func initValue() *int {  i := 4  return &i}

main Function , Called initValue function , This function returns a int Pointer and assign to n, The value corresponding to the pointer is 4. And then ,main Function call fmt.Println Printed pointer n / 2 Corresponding value . The program output is 2.

Program call initValue after , take i Assign the address of to the variable n . Be careful , If this time , Variable i On the stack , May be overwritten at any time .

Calling fmt.Println when ,Stack Frame Will be recreated , Variable i To be an assignment *n/2 That is to say 2, Will cover the original n The value of the variable pointed to . This can lead to extremely serious problems . In the face of sharing up Scene time ,go Variables are usually allocated to the heap , As shown in the figure below :

Through the above analysis , You can see that assigning objects to the stack when the called function returns a pointer type can cause serious problems , therefore Go Allocated variables to the heap . This distribution ensures the security of the program , But it also inevitably increases heap memory creation , And at some point in the future , need GC Clean up unused memory .

Principle of memory allocation


After the above analysis , You can simply sum up a few principles .

  • Sharing down typically stays on the stack Variables or objects created at the caller , Passed to the called function in the form of parameters , At this time , The memory space created at the caller is usually on the stack . This creates memory at the caller , When the callee uses the memory “ Memory sharing ” The way , be called Sharing down.
  • Sharing up typically escapes to the heap The object created in the called function , When it is returned to the caller as a pointer , Usually , The created memory space is on the heap . This is created by the callee , Used by the caller “ Memory sharing ” The way , be called Sharing up.
  • Only the compiler knows The reason why the above two principles both add the usual , Because of the specific distribution method , Is determined by the compiler , Some compiler backend optimizations , May break through these two principles , therefore , Specific allocation logic , Only the compiler ( Or people who develop compilers ) know .

Use go build The command determines the memory escape condition


It is worth noting that ,Go In determining whether a variable or object needs to escape to the heap , Is completed in the compiler ; in other words , When the code is written , After compiler compilation , Specific annotations will be made in binary , Declare that the specified variable is to be allocated to the heap or stack . You can print out the memory allocation logic at compile time using the following command , To know the memory allocation location of a specific variable or object .

see go help You can see go build It's actually calling go tool compile.

go help build ... -gcflags '[pattern=]arg list'        arguments to pass on each go tool compile invocation....
go tool compile -h...-m    print optimization decisions...-l    disable inlining...

among , There are two parameters that need to be concerned ,

  • -m Show optimization decisions
  • -l Inline is prohibited 【2】

The code is as follows :

package main
func main() {  n := initValue()  println(*n / 2)
  o := initObj()  println(o)
  f := initFn()  println(f)
  num := 5  result := add(num)  println(result)}
func initValue() *int {  i := 3                // ./main.go:19:2: moved to heap: i  return &i}
type Obj struct {  i int}
func initObj() *Obj {  return &Obj{i: 3}      // ./main.go:28:9: &Obj literal escapes to heap}
func initFn() func() {  return func() {       // ./main.go:32:9: func literal escapes to heap    println("I am a function")  }}
func add(i int) int {  return i + 1}

The complete build command and output are as follows :

go build -gcflags="-m -l" 
# _/Users/rocket/workspace/stack-or-heap./main.go:19:2: moved to heap: i./main.go:24:9: &Obj literal escapes to heap./main.go:28:9: func literal escapes to heap

You can see ,sharing up The situation of (initValue,initObj,initFn) Memory space is allocated to the heap .sharing down The situation of (add) Memory space is on the stack .

Here is a question for the reader , You can study moved to heap and escapes to heap The difference between .

summary


1. Because stack is more efficient than heap , Unwanted GC, therefore Go Will allocate memory to the stack as much as possible .

2. When allocated to the stack, it may cause illegal memory access and other problems , Can use heap , The main scenes are :

  1. When a value may be accessed after a function is called , This value is most likely allocated to the heap .
  2. When the compiler detects that a value is too large , This value will be allocated to the heap .
  3. When compiling , The compiler does not know the size of this value (slice、map...) This value will be allocated to the heap .

3.Sharing down typically stays on the stack

4.Sharing up typically escapes to the heap

5.Don't guess, Only the compiler knows

reference

【1】Go Language design and implementation :https://draveness.me/golang/docs/part3-runtime/ch07-memory/golang-stack-management/#%E5%AF%84%E5%AD%98%E5%99%A8

【2】Inlining optimisations in Go:https://dave.cheney.net/2020/04/25/inlining-optimisations-in-go

【3】Golang FAQ:https://golang.org/doc/faq#stack_or_heap

原网站

版权声明
本文为[Tencent cloud Ti platform]所创,转载请带上原文链接,感谢
https://yzsam.com/2021/08/20210813195634050d.html