Hi! Here is MelonTe.|

MelonTe

园龄:4个月粉丝:3关注:0

Golang逃逸现象

1、什么是内联函数?什么是逃逸现象?

什么是内联函数

内联函数是一种在编译时,直接将要调用的代码嵌入到调用处的优化技术,其主要目的是减少函数调用时的开销,例如对于普通函数其执行过程如下:

  • 将参数压入栈中
  • 根据地址跳转至对应位置执行
  • 执行完毕后返回调用点

而使用内联函数则将函数的代码嵌入到调用点,从而避免栈跳转等操作,使得程序更加迅速。在C++中,可以使用inline关键字显示指明,而Go中,会根据Go的编译器自动分析是否进行内联。

什么是逃逸现象

是一种用于分析对象引用的动态范围的优化技术,通常用于垃圾回收(GC)和内存管理的优化。在Go语言中,逃逸分析决定了一个对象的生命周期是局部的(只在栈上分配)还是全局的(需要在堆上分配)。如果一个变量的引用“逃逸”到函数外部,意味着它的生命周期无法被局部控制,就需要在堆上分配内存。

如何在Go中查看函数是否被内联优化呢?

首先,创建一个简单的demo。

package main

import "fmt"

// 声明内联函数
// Go语言本身没有显式的inline关键字,但编译器会自动进行内联优化
func add(a, b int) int {
    return a + b
}

func main() {
    x := 5
    y := 10

    // 调用内联函数
    result := add(x, y)

    fmt.Println("Result:", result)
}

使用-gcflags参数来查看内联优化,通过-m来输出信息。

go build -gcflags -m

输出如下内容:

# zmem
./main.go:7:6: can inline add
./main.go:16:15: inlining call to add
./main.go:18:13: inlining call to fmt.Println
./main.go:18:13: ... argument does not escape
./main.go:18:14: "Result:" escapes to heap
./main.go:18:25: result escapes to heap

可以看到add函数被内联调用了。

2、逃逸分析

我们先来看以下C++代码:

#include <iostream>
int *foo(int arg_val) {

    int foo_val = 11;

    return &foo_val;
}

int main()
{
    int *main_val = foo(666);

    printf("%d\n", *main_val);
}

进行编译并且运行,出现了以下信息:

$ gcc pro_1.c 
pro_1.c: In functionfoo’:
pro_1.c:7:12: warning: function returns address of local variable [-Wreturn-local-addr]
     return &foo_val;
            ^~~~~~~~
                
                
$ ./a.out 
段错误 (核心已转储)

程序发生了错误,其原因是因为foo函数试图返回一个函数内部局部变量的指针,这会导致变量的作用域跑出函数作用域,使得变量的生命周期无法被管控,这是不允许的(foo_val被定义在栈空间中,当函数结束该栈被销毁),发生了内存逃逸。

我们在Go中,也编写相同的代码:

package main

func foo(arg_val int)(*int) {

    var foo_val int = 11;
    return &foo_val;
}

func main() {

    main_val := foo(666)

    println(*main_val)
}

进行运行,打印出了:11

为什么在Go中可以运行,而在C++中不行呢?

其原因是Go会自动进行逃逸分析,即会自行判断变量的作用域是否会跑出局部函数中,若跑出了,则将该变量分配在中,否则就将变量分配在中。

这样子做的好处时可以使得开发者更专注于代码逻辑,而不用去关心具体的内存使用限制。

2.1、何时会发生逃逸?

1、返回变量的引用

对于以下代码:

package main

//go:noinline
func foo(arg_val int) *int {

	var foo_val1 int = 11
	var foo_val2 int = 12
	var foo_val3 int = 13
	var foo_val4 int = 14
	var foo_val5 int = 15
	println(&arg_val, &foo_val1, &foo_val2, &foo_val3, &foo_val4, &foo_val5)
	//返回foo_val3给main函数
	return &foo_val3
}

func main() {
	main_val := foo(666)

	println(*main_val, main_val)
}

使用go:noinline注释可以防止函数被自动内联,紧接着,调用该函数输出如下

0xc000067f28 0xc000067f08 0xc000067f00 0xc00000a038 0xc000067ef8 0xc000067ef0
13 0xc00000a038

可以看到返回的foo_val3的地址显然不与其他变量连续,进行跟踪输出如下:

go build -gcflags -m                                
# zmem                       "
./main.go:16:6: can inline main
./main.go:8:6: moved to heap: foo_val3

可以看到foo_val3逃逸到了堆空间中。

2、new的变量一定在栈中吗?

对于以下代码:

package main

//go:noinline
func foo(arg_val int) *int {

	var foo_val1 *int = new(int)
	var foo_val2 *int = new(int)
	var foo_val3 *int = new(int)
	var foo_val4 *int = new(int)
	var foo_val5 *int = new(int)
	println(arg_val, foo_val1, foo_val2, foo_val3, foo_val4, foo_val5)

	//返回foo_val3给main函数
	return foo_val3
}

func main() {
	main_val := foo(666)

	println(*main_val, main_val)
}

原始输出以及追踪输出如下:

666 0xc000067f00 0xc000067ef8 0xc00000a038 0xc000067ef0 0xc000067f08
0 0xc00000a038

PS C:\Users\minat\Desktop\Note\BrandnewBlog\go基础与底层\go-GC\zmem> go build -gcflags -m                                
# zmem                       "
./main.go:17:6: can inline main
./main.go:6:25: new(int) does not escape
./main.go:7:25: new(int) does not escape
./main.go:8:25: new(int) escapes to heap
./main.go:9:25: new(int) does not escape
./main.go:10:25: new(int) does not escape

可以看到,即使是new出的变量,也不一定是被放置在堆中,只有被返回才会被开辟在堆中。

3、引用类对象的引用类成员赋值

Go的引用类型有:func(函数类型),interface(接口类型),slice(切片类型),map(字典类型),channel(管道类型),*(指针类型)等。

当我们给一个引用类对象的引用类成员进行赋值,相当于要访问引用类成员的时候,是通过二次指针寻址,这时候会被判定为可能产生逃逸。

(1)[]interface{}数据类型,通过[]进行赋值

package main

func main() {
	data := []interface{}{100, 200}
	data[0] = 100
}
PS C:\Users\minat\Desktop\Note\BrandnewBlog\go基础与底层\go-GC\zmem> go build -gcflags -m
# zmem
./main.go:3:6: can inline main
./main.go:4:23: []interface {}{...} does not escape
./main.go:4:24: 100 does not escape
./main.go:4:29: 200 does not escape
./main.go:5:12: 100 escapes to heap

当赋值后,100逃逸到堆中。

(2)map[string]interface{}类型尝试通过赋值

package main

func main() {
    data := make(map[string]interface{})
    data["key"] = 200
}
./main.go:3:6: can inline main
./main.go:4:14: make(map[string]interface {}) does not escape
./main.go:5:16: 200 escapes to heap

(3)map[interface{}]interface{}类型尝试通过赋值

package main

func main() {
    data := make(map[interface{}]interface{})
    data[100] = 200
}
./main.go:3:6: can inline main
./main.go:4:14: make(map[interface {}]interface {}) does not escape
./main.go:5:7: 100 escapes to heap
./main.go:5:14: 200 escapes to heap

key和value都发生了逃逸

(4)map[string][]string数据类型进行赋值

package main

func main() {
    data := make(map[string][]string)
    data["key"] = []string{"value"}
}
./main.go:3:6: can inline main
./main.go:4:14: make(map[string][]string) does not escape
./main.go:5:24: []string{...} escapes to heap

赋值会导致value逃逸

(5)[]*int数据类型进行赋值

package main

func main() {
    a := 10
    data := []*int{nil}
    data[0] = &a
}
./main.go:3:6: can inline main
./main.go:4:2: moved to heap: a
./main.go:5:16: []*int{...} does not escape

赋值的右值发生了逃逸

3、小结

Golang中的一个局部变量,不管是否是通过new创建的,其是否会被分配在堆中取决于编译器的逃逸分析。

参考:https://www.yuque.com/aceld/golang/yyrlis

本文作者:MelonTe

本文链接:https://www.cnblogs.com/MelonTe/p/18622521

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   MelonTe  阅读(40)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起