首页 > 其他分享 >Golang逃逸现象

Golang逃逸现象

时间:2024-12-22 21:10:35浏览次数:5  
标签:foo val int Golang 逃逸 现象 func go main

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

相关文章

  • Golang学习笔记_16——Map
    Golang学习笔记_13——数组Golang学习笔记_14——切片Golang学习笔记_15——range文章目录Map1.介绍2.声明和初始化3.类型4.基本操作4.1插入更新4.2访问值4.3删除4.4遍历5.注意事项6.示例Map1.介绍在Go语言中,map是一种内置的数据结构,用于存储键......
  • 假设三:走势包含无序运动状态(混沌现象)和有序运动状态(下跌或上涨)
    混沌现象,在经济学中是被广泛研究的一个类现象,它主要是指经济系统中产生的貌似随机的动态行为。资本市场例如股票或期货市场,通常被视为一个混沌系统,这类系统在一些时间段内是处于无序运动的状态。因而,我们作为投资者,需要基于混沌系统中无序运动的基本特征来认识资本市场。普......
  • Golang中的Map是怎么遍历的
    在Golang中,遍历map的常见方法是使用for...range循环。map是无序的键值对集合,因此遍历map时,每次迭代访问的键值对顺序可能不同。以下是一个遍历map的示例:packagemainimport"fmt"funcmain(){//创建一个mapmyMap:=map[string]int{"ap......
  • Golang学习笔记_13——数组
    Golang学习笔记_10——SwitchGolang学习笔记_11——指针Golang学习笔记_12——结构体文章目录数组1.定义2.访问和修改3.多维数组4.计算数组长度5.数组作为函数参数6.遍历7.数组的内存表示源码数组Go语言中的数组是一种具有固定长度、相同类型元素的集......
  • Golang学习笔记_14——切片
    Golang学习笔记_11——指针Golang学习笔记_12——结构体Golang学习笔记_13——数组文章目录切片1.定义2.创建3.基本操作4.动态性5.子切片6.数组和切片7.注意8.高级用法源码切片Go语言中的切片(slice)是一种非常强大且灵活的数据结构,它基于数组,但提供了......
  • Golang学习历程【第四篇 运算符&流程控制】
    Golang学习历程【第四篇运算符&流程控制】1.运算符1.1算术运算符1.2关系运算符1.3逻辑运算符1.4赋值运算符1.5其他运算符2.二进制运算3.流程控制3.1条件表达式if3.2for循环3.3forrange(键值循环)3.4switch...case3.5break,continue,goto1.运算符1.......
  • golang:第三方库:用vipper解析yaml配置文件
    一,安装第三方库$gogetgithub.com/spf13/viper二,代码1,配置文件Database:DBType:mysqlUserName:dbusernamePassword:dbpasswordHost:127.0.0.1:3306DBName:dbnameCharset:utf8ParseTime:TrueMaxIdleConns:10MaxOpenConns:30 2,代码:......
  • 2024 GoLang安装使用教程(附激活以及常见问题处理)
    第一步:下载GoLang安装包访问GoLang官网,下载GoLang第二步:安装GoLang下载完成后,进行安装,next,安装完成点击xx关掉程序!第三步:下载补丁GoLang补丁文件点击获取补丁下载成功后,打开标注的文件文件夹,进入到文件夹/jetbra注意:这个文件夹单独copy一份,所属文件夹......
  • 关于大模型偶尔抽风的现象,以及大模型与具体应用结合的思考
    “解决问题是一个人能力的体现,不论是在职场还是在生活中**”**最近在对接GPT做一个图生文的功能,简单来说就是让大模型理解图像,然后做一些图像解析或反推提示词的效果。在基础功能开发完成之后,然后让测试人员开始功能测试,然后就发现了一些问题;最常见的就是大模型抽风的问......
  • golang单元测试和mock框架的介绍和推荐
    背景介绍:探索golang的单元测试框架,看一下哪种框架是结合业务体验更好的。推荐和不推荐使用的框架,我都会在标题中标注出来,没有标注的表示体验一般,但也没有特别的缺点,观望态度单元测试框架介绍原生testing示例funcTestModifyArr(t*testing.T){ arr:=[3]int{0,1,2}......