Go语言中的逃逸分析作用

简介: Go内存逃逸分析,讲述Go如何管理变量的内存的。

前 言

很多时候为了更快的开发效率,大多数程序员都是在使用抽象层级更高的技术,包括语言,框架,设计模式等。所以导致很多程序员包括我自己在内对于底层和基础的知识都会有些生疏和,但是正是这些底层的东西构建了我们熟知的解决方案,同时决定了一个技术人员的上限。

在写C和C++的时候动态分配内存是让程序员自己手动管理,这样做的好处是,需要申请多少内存空间可以很好的掌握怎么分配,但是如果忘记释放内存,则会导致内存泄漏。

Rust又比👆上面俩门语言分配内存方式显得不同,Rust的内存管理主要特色可以看做是编译器帮你在适当的地方插入delete来释放内存,这样一来你不需要显式指定释放,runtime也不需要任何GC,但是要做到这点,编译器需要能分析出在什么地方delete,这就需要你代码按照其规则来写了。

相比上面几种的内存管理方式的语言,像JavaGolang在语言设计的时候就加入了garbage collection也就runtime中的gc,让程序员不需要自己管理内存,真正解放了程序员的双手,让我们可以专注于编码。

函数栈帧

你的程序怎么跑起来的👆

当一个函数在运行时,需要为它在堆栈中创建一个栈帧(stack frame)用来记录运行时产生的相关信息,因此每个函数在执行前都会创建一个栈帧,在它返回时会销毁该栈帧。

stack frame准确来说应该是call stack

通常用一个叫做栈基址(bp)的寄存器来保存正在运行函数栈帧的开始地址,由于栈指针(sp)始终保存的是栈顶的地址,所以栈指针保存的也就是正在运行函数栈帧的结束地址。

销毁栈帧

销毁时先把栈指针(sp)移动到此时栈基址(bp)的位置,此时栈指针和栈基址都指向同样的位置。

Go内存逃逸

可以简单得理解成一次函数调用内部申请到的内存,它们会随着函数的返回把内存还给系统。下面来看看一个例子:

package main

import "fmt"

func main() {
    f := foo("Ding")
    fmt.Println(f)
}

type bar struct {
    s string
}

func foo(s string) bar {
    f := new(bar) // 这里的new(bar)会不会发生逃逸???
    defer func() {
        f = nil
    }()
    f.s = s
    return *f
}

我想很多人认为发生了逃逸,但是真的是这样的吗?那就用go build -gcflags=-m escape/struct.go看看会输出什么???

escape/struct.go:7:13: f escapes to heap 可以省略

其实没有发生逃逸,而escape/struct.go:7:13: f escapes to heap的逃逸是因为动态类型逃逸fmt.Println(a …interface{})在编译期间很难确定其参数的具体类型,也能产生逃逸。

继续看下面这一个例子:

package main

import "fmt"

func main() {
    f := foo("Ding")
    fmt.Println(f)
}

type bar struct {
    s string
}

func foo(s string) *bar {
    f := new(bar) // 这里的new(bar)会不会发生逃逸???
    defer func() {
        f = nil
    }()
    f.s = s
    return f
}

f := new(bar)会发生逃逸吗?

$: go build -gcflags=-m escape/struct.go
# command-line-arguments
escape/struct.go:16:8: can inline foo.func1
escape/struct.go:7:13: inlining call to fmt.Println
escape/struct.go:14:10: leaking param: s
escape/struct.go:15:10: new(bar) escapes to heap ✅
escape/struct.go:16:8: func literal does not escape
escape/struct.go:7:13: []interface {}{...} does not escape
<autogenerated>:1: .this does not escape

Go可以返回局部变量指针,这其实是一个典型的变量逃逸案例,虽然在函数 foo() 内部 f 为局部变量,其值通过函数返回值返回,f 本身为一指针,其指向的内存地址不会是栈而是堆,这就是典型的逃逸案例。

那就继续往下看吧,看看这个例子:

package main

func main() {
    Slice() // ??? 会发生逃逸吗?
}

func Slice() {
    s := make([]int, 10000, 10000)

    for index, _ := range s {
        s[index] = index
    }
}

估计很多人会回答没有,其实这里发生逃逸,实际上当栈空间不足以存放当前对象时或无法判断当前切片长度时会将对象分配到堆中。

发生逃逸

最后一个例子:

package main

import (
    "fmt"
    "io"
    "os"
)

func main() {
    Println(string(*ReverseA("Ding Ding"))) // ???
}


func Println(str string) {
    io.WriteString(os.Stdout,
      str+"\n")
}

func ReverseA(str string) *[]rune {
    result := make([]rune, 0, len(str))
    for _, v := range []rune(str) {
        v := v
        defer func() {
            result = append(result, v)
        }()
    }
    return &result
}

如果一个变量被取地址,通过函数返回指针值返回,还有闭包,编译器不确定你的切片容量时,是否要扩容的时候,放到堆上,以致产生逃逸。

于是我优化了一下代码,再看看

package main

import (
    "io"
    "os"
)

func main() {
    result := []rune("Ding Ding")
    ReverseB(result)
    Println(string(result))
}

func ReverseB(runes []rune) {
    for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
        runes[i], runes[j] = runes[j], runes[i]
    }
}

func Println(str string) {
    io.WriteString(os.Stdout,
      str+"\n")
}

str在运行的时候可更改

如何得知变量是怎么分配?

引用 (golang.org) FAQ官方说的:

准确地说,你并不需要知道,Golang 中的变量只要被引用就一直会存活,存储在堆上还是栈上由内部实现决定而和具体的语法没有关系。知道变量的存储位置确实和效率编程有关系。如果可能,Golang 编译器会将函数的局部变量分配到函数栈帧(stack frame)上, 然而,如果编译器不能确保变量在函数 return之后不再被引用,编译器就会将变量分配到堆上。而且,如果一个局部变量非常大,那么它也应该被分配到堆上而不是栈上。

小 结

  • 逃逸分析的好处是为了减少gc的压力
  • 栈上分配的内存不需要gc处理
  • 同步消除,如果你定义的对象的方法上有同步锁,但在运行时,却只有一个线程在访问,此时逃逸分析后的机器码,会去掉同步锁运行。
目录
相关文章
|
2月前
|
存储 安全 Java
【Golang】(4)Go里面的指针如何?函数与方法怎么不一样?带你了解Go不同于其他高级语言的语法
结构体可以存储一组不同类型的数据,是一种符合类型。Go抛弃了类与继承,同时也抛弃了构造方法,刻意弱化了面向对象的功能,Go并非是一个传统OOP的语言,但是Go依旧有着OOP的影子,通过结构体和方法也可以模拟出一个类。
208 1
|
4月前
|
Cloud Native Go API
Go:为云原生而生的高效语言
Go:为云原生而生的高效语言
402 0
|
4月前
|
Cloud Native Java Go
Go:为云原生而生的高效语言
Go:为云原生而生的高效语言
262 0
|
4月前
|
Cloud Native Java 中间件
Go:为云原生而生的高效语言
Go:为云原生而生的高效语言
231 0
|
4月前
|
Cloud Native Java Go
Go:为云原生而生的高效语言
Go:为云原生而生的高效语言
336 0
|
编译器 程序员 Go
Golang中逃逸现象, 变量“何时栈?何时堆?”
Golang中一个函数内局部变量,不管是不是动态new出来的,它会被分配在堆还是栈,是由编译器做逃逸分析之后做出的决定。
381 0
|
10月前
|
编译器 Go
揭秘 Go 语言中空结构体的强大用法
Go 语言中的空结构体 `struct{}` 不包含任何字段,不占用内存空间。它在实际编程中有多种典型用法:1) 结合 map 实现集合(set)类型;2) 与 channel 搭配用于信号通知;3) 申请超大容量的 Slice 和 Array 以节省内存;4) 作为接口实现时明确表示不关注值。此外,需要注意的是,空结构体作为字段时可能会因内存对齐原因占用额外空间。建议将空结构体放在外层结构体的第一个字段以优化内存使用。
|
10月前
|
运维 监控 算法
监控局域网其他电脑:Go 语言迪杰斯特拉算法的高效应用
在信息化时代,监控局域网成为网络管理与安全防护的关键需求。本文探讨了迪杰斯特拉(Dijkstra)算法在监控局域网中的应用,通过计算最短路径优化数据传输和故障检测。文中提供了使用Go语言实现的代码例程,展示了如何高效地进行网络监控,确保局域网的稳定运行和数据安全。迪杰斯特拉算法能减少传输延迟和带宽消耗,及时发现并处理网络故障,适用于复杂网络环境下的管理和维护。
|
4月前
|
Cloud Native 安全 Java
Go:为云原生而生的高效语言
Go:为云原生而生的高效语言
303 1
|
4月前
|
数据采集 Go API
Go语言实战案例:多协程并发下载网页内容
本文是《Go语言100个实战案例 · 网络与并发篇》第6篇,讲解如何使用 Goroutine 和 Channel 实现多协程并发抓取网页内容,提升网络请求效率。通过实战掌握高并发编程技巧,构建爬虫、内容聚合器等工具,涵盖 WaitGroup、超时控制、错误处理等核心知识点。

热门文章

最新文章