gol memory escape

Keywords: Go

summary

  • The escaped pointer variable itself is also allocated in the heap space, so the function can return the address of the local variable. At this time, the local variable is equivalent to the local pointer variable. When escaping, the pointer variable itself is also allocated in the heap space, so it can return its address.
  • The memory of stack space is managed by the compiler, and the allocation and release speed is very fast. Heap space is managed by gc. Frequent gc will occupy a large system overhead. stop the world
  • Escape analysis is done by the compiler at static compilation time.
  • If the slice variable itself escapes, its underlying data area will also escape. Even if the slice length is small.
  • The slice variable itself does not escape. Generally, its data area is also on the stack. If the length is too long, the data area will be allocated to the heap, but the slice variable itself is still on the stack.

How to determine memory escape?

go run -gcflags '-m -l' main.go

Note: the above command only has output when the compiler accesses and judges whether to escape.
Sometimes the stack space is smaller than the heap space address. I don't know why.

Examples of memory escape

func main()  {
    a := 1
    _ = a
}

// No output
func main()  {
    a := 1
    fmt.Printf("%p\n", &a)
    fmt.Println(a)

    b := 2
    fmt.Printf("%p\n", &b)
    fmt.Println(b)

    c := 3
    fmt.Printf("%p\n", &c)
    fmt.Println(c)

    d := 4
    fmt.Printf("%p\n", &d)
    fmt.Println(d)
}

// output
./main.go:10:2: moved to heap: a
./main.go:14:2: moved to heap: b
./main.go:18:2: moved to heap: c
./main.go:22:2: moved to heap: d
./main.go:11:12: ... argument does not escape
./main.go:12:13: ... argument does not escape
./main.go:12:13: a escapes to heap
./main.go:15:12: ... argument does not escape
./main.go:16:13: ... argument does not escape
./main.go:16:13: b escapes to heap
./main.go:19:12: ... argument does not escape
./main.go:20:13: ... argument does not escape
./main.go:20:13: c escapes to heap
./main.go:23:12: ... argument does not escape
./main.go:24:13: ... argument does not escape
./main.go:24:13: d escapes to heap
0xc000012080
1
0xc000012088
2
0xc0000120a0
3
0xc0000120a8
4

fmt.Printf function abc is assigned to interface {}, causing escape. The abc address is growing in the heap space, as shown in the figure, which confirms that it is on the heap. Compare the following:

func main()  {
    a := 1
    println(&a)
    println(a)

    b := 2
    println(&b)
    println(b)

    c := 3
    println(&c)
    println(c)

    d := 4
    println(&d)
    println(d)
}

// output
0xc00002e768
1
0xc00002e760
2
0xc00002e758
3
0xc00002e750
4

println does not trigger memory escape. abc is allocated in stack space, which is confirmed by the increase of address from high to low.

The four pointers do not escape:

func main()  {
    a := new(int)
    println(&a)
    println(a)

    println("======")
    b := new(int)
    println(&b)
    println(b)

    println("======")
    c := new(int)
    println(&c)
    println(c)

    println("======")
    d := new(int)
    println(&d)
    println(d)
}

// output
./main.go:32:10: new(int) does not escape
./main.go:39:10: new(int) does not escape
./main.go:46:10: new(int) does not escape
./main.go:53:10: new(int) does not escape
0xc00002e768
0xc00002e740
======
0xc00002e760
0xc00002e738
======
0xc00002e758
0xc00002e730
======
0xc00002e750
0xc00002e748

The abcd pointer variable itself and the memory area it refers to do not escape. They are allocated in the stack space. The printed address is a continuous area from large to small, which confirms this.
The memory allocation is shown in the figure below:

The four pointers have escaped

func main() {
    a := new(int)
    fmt.Printf("%p\n", &a)
    fmt.Println(a)
    println("======")

    b := new(int)
    fmt.Printf("%p\n", &b)
    fmt.Println(b)

    println("======")
    c := new(int)
    fmt.Printf("%p\n", &c)
    fmt.Println(c)
    println("======")

    d := new(int)
    fmt.Printf("%p\n", &d)
    fmt.Println(d)

    println("======")
    f := new(int)
    println(&f)
    println(f)
}

//output
0xc00000e028
0xc000012080
======
0xc00000e038
0xc000012088
======
0xc00000e040
0xc0000120a0
======
0xc00000e048
0xc0000120a8
======
0xc000058f38
0xc000058f30

In the case of escape, the abcd pointer variable itself and the memory area it refers to are allocated in the heap space, and the printed address is a continuous area from small to large. The f pointer variable and the memory area it refers to are allocated in the stack space, and its address is higher than the heap area address, which proves this.
The escaped pointer variable itself is also allocated in heap space, so the function can return local pointer variables
The memory allocation diagram is as follows:

func main()  {
    a := new(int)
    _ = a
}

// output
./main.go:8:10: main new(int) does not escape

Even if the new variable does not escape, it is allocated on the stack

  • The println function does not trigger escape, but fmt.Printf does because its function parameter is of interface {} type
  • Note: in the above example, a is the pointer variable allocated in the stack space, and the int storage unit pointed to is in the next byte (also in the stack space).
  • Stack space is allocated from high to low at high address.

If this is written in C + +, it is a typical error: return the address of the local variable, and the content of the address will be automatically released after the function exits, because it is on the stack.

So is the local variable of go language on the stack or on the heap? The go language compiler will do escape analysis to analyze whether the scope of local variables escapes from the scope of the function. If not, it will be placed on the stack; If the scope of the variable exceeds the scope of the function, it is automatically placed on the heap.

Test and observe the address change of g

func main {
    var g *int
    println(&g)
    println(g)
    g = new(int)
    println(&g)
    println(g)
    g = new(int)
    println(&g)
    println(g)
    g = new(int)
    fmt.Println(&g)
    fmt.Println(g)
}

Without the last two lines, there is no escape. The memory pointed to by g and g is in the stack area, and if added, it is in the heap area.

There is a scene of escape

package main

type Student struct {
 Name interface{}
}

func main()  {
 stu := new(Student)
 stu.Name = "tom"

}

Escape occurs when interface {} is assigned. The optimization scheme is to set the type to a fixed type, such as string

package main

type Student struct {
 Name string
}

func GetStudent() *Student {
 stu := new(Student)
 stu.Name = "tom"
 return stu
}

func main() {
 GetStudent()
}

If the pointer type (address of local variable) is returned, escape will occur, and the optimization scheme depends on the situation.
Is it efficient for functions to pass pointers and values? We know that passing the pointer can reduce the copy of the underlying value and improve the efficiency. However, if the amount of copied data is small, the pointer passing will cause escape, which may use the heap or increase the burden of GC. Therefore, passing the pointer is not necessarily efficient.
Do not blindly use variable pointers as parameters. Although replication is reduced, the overhead of variable escape may be greater.

func main()  {
    nums := make([]int, 5, 5)
    nums2 := make([]int, 5, 5)
    println(&nums)
    println(nums)
    println(&nums2)
    println(nums2)
    for i := range nums {
        nums[i] = i
        println(&nums[i])
    }
    println("======2")
    for i := range nums2 {
        nums2[i] = i
        println(&nums2[i])
    }

    println("======3")
    nums3 := make([]int, 10000, 10000)
    println(&nums3)
    println(nums3)
    for i := range nums3 {
        if i == 5 {
            break
        }
        nums3[i] = i
        println(&nums3[i])
    }
    //fmt.Println(&nums3)

}

// output
./main.go:8:14: make([]int, 5, 5) does not escape
./main.go:9:15: make([]int, 5, 5) does not escape
./main.go:25:15: make([]int, 10000, 10000) escapes to heap
0xc00002e758
[5/5]0xc00002e6f8
0xc00002e740
[5/5]0xc00002e6d0
0xc00002e6f8
0xc00002e700
0xc00002e708
0xc00002e710
0xc00002e718
======2
0xc00002e6d0
0xc00002e6d8
0xc00002e6e0
0xc00002e6e8
0xc00002e6f0
======3
0xc00002e728
[10000/10000]0xc000054000
0xc000054000
0xc000054008
0xc000054010
0xc000054018
0xc000054020

As above: the first and second slice variables and their underlying data pointer areas are allocated on the stack.
For the third super slice, the slice variable itself is allocated on the stack, and the underlying data is on the heap.
If the last fmt.Printf is commented out, the third slice variable itself will escape and be allocated on the heap.

Note: the address space of the 1st to nth elements in the slice data area increases from 0 to up, whether on the stack or on the heap.

func main()  {
    var slice1 []int
    println(&slice1)
    println(slice1)
    fmt.Printf("%p\n", &slice1)
    println(&slice1)
    println(slice1)
    slice1 = make([]int,5,5)
    println(&slice1)
    println(slice1)
}

// output
./main.go:11:6: moved to heap: slice1
./main.go:14:12: ... argument does not escape
./main.go:17:15: make([]int, 5, 5) escapes to heap
0xc0000a4018
[0/0]0x0
0xc0000a4018
0xc0000a4018
[0/0]0x0
0xc0000a4018
[5/5]0xc0000aa030

If the slice variable itself escapes, its underlying data area will also escape. Even if the slice length is small.
The slice variable itself does not escape. Generally, its data area is also on the stack. If the length is too long, the data area will be allocated to the heap, but the slice variable itself is still on the stack.

package main

func main() {
 nums := make([]int, 10000, 10000)

 for i := range nums {
  nums[i] = i
 }
}

If the slice is too large, escape will occur. Set the capacity as much as possible in the optimization scheme. If the capacity is too large, there is no way.

Elements in a map cannot be addressed.

When the map does not escape, it is also allocated on the stack, the variable itself and the underlying data area.

Posted by akaki on Thu, 25 Nov 2021 13:58:49 -0800