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 function ‘foo’:
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
标签:foo,val,int,Golang,逃逸,现象,func,go,main From: https://www.cnblogs.com/MelonTe/p/18622521