Go escape analysis variables in heap or stack

catalogue

Go escape analysis variables in heap or stack

References

https://www.youtube.com/watch?v=3D4o0MVs4Qo

https://www.kancloud.cn/aceld/golang/1958306

https://u.geekbang.org/subject/go?utm_identify=geektime&gk_cus_user_wechat=university

Stack memory

​ In computer system, heap and stack are abstracted areas in memory for storing program data

  • The advantage of stack is faster access speed, but the disadvantage is lack of flexibility; The size and life cycle of the data in the stack are determined and consistent with the function. It is generally used to store function parameters and local variables, which are automatically allocated and released by the compiler
  • The advantage of heap is that it can dynamically allocate memory size, which is suitable for memory allocation of unpredictable size, and has autonomy in the life cycle. The disadvantage is that complex memory allocation management will occupy resources, slow speed, memory fragmentation and heap memory leakage

Escape Analysis

According to the different characteristics of heap and stack, it is necessary to select an appropriate area to store the data in the program

For example, the memory management in C/C + + needs to be managed by programmers and developers

Most modern languages entrust the complex and error prone memory management to the compiler and parser, which makes the stack memory allocation transparent to programmers and reduces the development burden

One of the better technologies is escape analysis. The compiler can analyze the code characteristics and decide whether to use the heap or stack. As the name suggests, it is to escape the data that should be stored in the stack to the heap

The implementation of escape analysis technology in each language is similar. Java can also enable escape analysis through JVM parameters

Code case

Escape case of C code

There is no escape analysis in C language, so an error will be reported here. There is a wild pointer

#include<stdio.h>

int* returnAddr()
{
	//After the function ends, the stack memory will be released, and the pointer in memory returned here will become a wild pointer
	int a = 3;
	return &a; //Returns the memory address of a local variable
}

int main()
{
	return 0;
}

Execution result, error: forbidden to return local variables

test.cc:7:10: warning: address of stack memory associated with local variable 'a' returned [-Wreturn-stack-address]
        return &a; //Returns the memory address of a local variable
                ^
1 warning generated.

Go code case

escape analysis

The code can run normally

The go compiler will decide whether the variables are stored on the heap or stack, which is escape analysis

If the scope of the variable does not run out of the function range, it can be on the stack, otherwise it is allocated on the heap

package main

//Go build - gcflags' - M - L '. / main.go / / print escape analysis
//Go tool compile - S. / main. Go / / print assembly code
//Go tool compile - M. / main. Go / / print escape analysis

func returnAddr() *int {
	//Escape to heap memory occurs
	a := 1
	return &a //Returns a pointer to a local variable
}

func returnAddr1() int {
	//Will not escape to heap memory
	a := new(int)
	return *a //Returns the value of a pointer to a local variable
}

func main() {
	returnAddr()
	returnAddr1()
}

Execution result analysis

# You can see the prompt in line 8 that the variable is moved to heap (moved to heap: a)

$ go build -gcflags '-m -l' ./main.go
# command-line-arguments
./main.go:8:2: moved to heap: a
./main.go:14:10: new(int) does not escape

Print assembly list to standard output

# Search the value of runtime.newobject keyword in heap space
$ go tool compile -S main.go
"".returnAddr STEXT size=79 args=0x8 locals=0x18 funcid=0x0
	0x0000 00000 (main.go:6)	TEXT	"".returnAddr(SB), ABIInternal, $24-8
	0x0000 00000 (main.go:6)	MOVQ	(TLS), CX
	0x0009 00009 (main.go:6)	CMPQ	SP, 16(CX)
	0x000d 00013 (main.go:6)	PCDATA	$0, $-2
	0x000d 00013 (main.go:6)	JLS	72
	0x000f 00015 (main.go:6)	PCDATA	$0, $-1
	0x000f 00015 (main.go:6)	SUBQ	$24, SP
	0x0013 00019 (main.go:6)	MOVQ	BP, 16(SP)
	0x0018 00024 (main.go:6)	LEAQ	16(SP), BP
	0x001d 00029 (main.go:6)	FUNCDATA	$0, gclocals·2a5305abe05176240e61b8620e19a815(SB)
	0x001d 00029 (main.go:6)	FUNCDATA	$1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
	0x001d 00029 (main.go:8)	LEAQ	type.int(SB), AX
	0x0024 00036 (main.go:8)	MOVQ	AX, (SP)
	0x0028 00040 (main.go:8)	PCDATA	$1, $0
	0x0028 00040 (main.go:8)	CALL	runtime.newobject(SB)
	0x002d 00045 (main.go:8)	MOVQ	8(SP), AX
	0x0032 00050 (main.go:8)	MOVQ	$1, (AX)
	0x0039 00057 (main.go:9)	MOVQ	AX, "".~r0+32(SP)
	0x003e 00062 (main.go:9)	MOVQ	16(SP), BP
	0x0043 00067 (main.go:9)	ADDQ	$24, SP
	0x0047 00071 (main.go:9)	RET
	0x0048 00072 (main.go:9)	NOP
	0x0048 00072 (main.go:6)	PCDATA	$1, $-1
	0x0048 00072 (main.go:6)	PCDATA	$0, $-2
	0x0048 00072 (main.go:6)	CALL	runtime.morestack_noctxt(SB)
	0x004d 00077 (main.go:6)	PCDATA	$0, $-1
	0x004d 00077 (main.go:6)	JMP	0
	0x0000 65 48 8b 0c 25 00 00 00 00 48 3b 61 10 76 39 48  eH..%....H;a.v9H
	0x0010 83 ec 18 48 89 6c 24 10 48 8d 6c 24 10 48 8d 05  ...H.l$.H.l$.H..
	0x0020 00 00 00 00 48 89 04 24 e8 00 00 00 00 48 8b 44  ....H..$.....H.D
	0x0030 24 08 48 c7 00 01 00 00 00 48 89 44 24 20 48 8b  $.H......H.D$ H.
	0x0040 6c 24 10 48 83 c4 18 c3 e8 00 00 00 00 eb b1     l$.H...........
	rel 5+4 t=17 TLS+0
	rel 32+4 t=16 type.int+0
	rel 41+4 t=8 runtime.newobject+0
	rel 73+4 t=8 runtime.morestack_noctxt+0
"".returnAddr1 STEXT nosplit size=10 args=0x8 locals=0x0 funcid=0x0
	0x0000 00000 (main.go:12)	TEXT	"".returnAddr1(SB), NOSPLIT|ABIInternal, $0-8
	0x0000 00000 (main.go:12)	FUNCDATA	$0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
	0x0000 00000 (main.go:12)	FUNCDATA	$1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
	0x0000 00000 (main.go:15)	MOVQ	$0, "".~r0+8(SP)
	0x0009 00009 (main.go:15)	RET
	0x0000 48 c7 44 24 08 00 00 00 00 c3                    H.D$......
"".main STEXT nosplit size=1 args=0x0 locals=0x0 funcid=0x0
	0x0000 00000 (main.go:18)	TEXT	"".main(SB), NOSPLIT|ABIInternal, $0-0
	0x0000 00000 (main.go:18)	FUNCDATA	$0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
	0x0000 00000 (main.go:18)	FUNCDATA	$1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
	0x0000 00000 (main.go:20)	RET
	0x0000 c3                                               .
go.cuinfo.packagename. SDWARFCUINFO dupok size=0
	0x0000 6d 61 69 6e                                      main
""..inittask SNOPTRDATA size=24
	0x0000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
	0x0010 00 00 00 00 00 00 00 00                          ........
go.info."".returnAddr$abstract SDWARFABSFCN dupok size=24
	0x0000 04 2e 72 65 74 75 72 6e 41 64 64 72 00 01 01 0c  ..returnAddr....
	0x0010 61 00 08 00 00 00 00 00                          a.......
	rel 0+0 t=24 type.*int+0
	rel 0+0 t=24 type.int+0
	rel 19+4 t=31 go.info.int+0
go.info."".returnAddr1$abstract SDWARFABSFCN dupok size=25
	0x0000 04 2e 72 65 74 75 72 6e 41 64 64 72 31 00 01 01  ..returnAddr1...
	0x0010 0c 61 00 0e 00 00 00 00 00                       .a.......
	rel 0+0 t=24 type.*int+0
	rel 0+0 t=24 type.int+0
	rel 20+4 t=31 go.info.*int+0
gclocals·2a5305abe05176240e61b8620e19a815 SRODATA dupok size=9
	0x0000 01 00 00 00 01 00 00 00 00                       .........
gclocals·33cdeccccebe80329f1fdbee7f5874cb SRODATA dupok size=8
	0x0000 01 00 00 00 00 00 00 00                          ........

Escape cases

In addition to the return local pointer above, there will also be escape when assigning a value to a reference class member in a reference object

It can be understood that when accessing a reference object, the underlying layer is actually accessed indirectly through a pointer, but if you access the reference members inside again, there will be a second indirect access. In this way, the operation of this part of the object may greatly escape

The reference types of Go are func,interface,slice,map,channel and pointer

Case summary

package main

import (
	"log"
)

func demo(a *int) {
	return
}

func demo1(a []string) {
	return
}

func main() {
	/**
	  Escape cases
	*/
	//Case 1
	//For [] interface {} data type, escape must occur through [] assignment
	data1 := []interface{}{100, 200}
	data1[0] = 100

	//Case 2
	//If the map[string]interface {} type attempts to assign a value, it must escape
	data2 := make(map[string]interface{})
	data2["key"] = 200

	//Case 3
	//map[interface{}]interface {} type attempts to assign values, which will lead to the assignment of key and value and escape
	data3 := make(map[interface{}]interface{})
	data3[100] = 200

	//Case 4
	//map[string][]string data type, assignment will cause escape
	data4 := make(map[string][]string)
	data4["key"] = []string{"value"}

	//Case 5
	//[] * int data type, the assigned right value will escape
	a := 10
	data5 := []*int{nil}
	data5[0] = &a

	//Case 6
	//func(*int) function type. If the function is assigned, the passed formal parameters will escape
	data6 := 10
	f := demo
	f(&data6)
	log.Println(data6)

	//Case 7
	//func([]string): function type. Assigning [] string{"value"} will make the passed parameters escape
	s := []string{"aceld"}
	demo1(s)
	log.Println(s)

	//Case 8
	//Channel [] string data type. If [] string{"value"} is transmitted in the current channel, escape will occur
	ch := make(chan []string)
	s1 := []string{"aceld"}
	go func() {
		ch <- s1
	}()

	//Case 9
	//Function returns a local pointer and an escape occurs
	func() *int {
		a := 1
		return &a
	}()
}

Results of performing escape analysis

$ go tool compile -m main.go
main.go:7:6: can inline demo
main.go:11:6: can inline demo1
main.go:49:3: inlining call to demo
main.go:55:7: inlining call to demo1
main.go:62:5: can inline main.func1
main.go:7:11: a does not escape
main.go:11:12: a does not escape
main.go:41:2: moved to heap: a
main.go:21:24: []interface {}{...} does not escape
main.go:21:25: 100 does not escape
main.go:21:30: 200 does not escape
main.go:22:11: 100 escapes to heap
main.go:26:15: make(map[string]interface {}) does not escape
main.go:27:15: 200 escapes to heap
main.go:31:15: make(map[interface {}]interface {}) does not escape
main.go:32:7: 100 escapes to heap
main.go:32:13: 200 escapes to heap
main.go:36:15: make(map[string][]string) does not escape
main.go:37:25: []string{...} escapes to heap
main.go:42:17: []*int{...} does not escape
main.go:50:13: ... argument does not escape
main.go:50:13: data6 escapes to heap
main.go:54:15: []string{...} escapes to heap
main.go:56:13: ... argument does not escape
main.go:56:13: s escapes to heap
main.go:61:16: []string{...} escapes to heap
main.go:62:5: func literal escapes to heap

Escape case I

For [] interface {} data type, escape must occur through [] assignment

package main

func main() {
    data := []interface{}{100, 200}
    data[0] = 100//escape
}

Escape case II

If the map[string]interface {} type attempts to assign a value, it must escape

package main

func main() {
    data := make(map[string]interface{})
    data["key"] = 200//escape
}

Escape case III

map[interface{}]interface {} type attempts to assign values, which will lead to the assignment of key and value and escape

package main

func main() {
    data := make(map[interface{}]interface{})
    data[100] = 200//Both 100 and 200 escaped
}

Escape case 4

map[string][]string data type, assignment will cause escape

package main

func main() {
    data := make(map[string][]string)//Will escape to the pile
    data["key"] = []string{"value"}
}

Escape case 5

[] * int data type, the assigned right value will escape

package main

func main() {
    a := 10//escape
    data := []*int{nil}
    data[0] = &a
}

Escape case 6

func(*int) function type. If the function is assigned, the passed formal parameters will escape

package main

import "fmt"

func foo(a *int) {
    return
}

func main() {
    data := 10//escape
    f := foo
    f(&data)
    fmt.Println(data)
}

Escape case 7

func([]string): function type. Assigning [] string{"value"} will make the passed parameters escape

package main

import "fmt"

func foo(a []string) {
    return
}

func main() {
    s := []string{"aceld"}//escape
    foo(s)
    fmt.Println(s)
}

Escape case 8

Channel [] string data type. If [] string{"value"} is transmitted in the current channel, escape will occur

package main

func main() {
    ch := make(chan []string)

    s := []string{"aceld"}//escape

    go func() {
        ch <- s
    }()
}

summary

In other words, the local variables in the function, whether dynamically new or not, will be allocated to the stack or heap, which is determined by the compiler after escape analysis

Generally, externally referenced variables will be put into heap memory. If the compiler is not sure whether they will be externally referenced, they will also be put into heap memory

If you assign a value to a reference class member in a reference object, you will also escape

Keywords: Go

Added by mbaroz on Tue, 02 Nov 2021 11:30:27 +0200