郑文峰的博客 郑文峰的博客
首页
  • Go语言高性能编程
分类
标签
归档
关于
  • 导航 (opens new window)
  • 代码片段 (opens new window)
  • 收藏
  • 友链
  • 外部页面

    • 开往 (opens new window)
GitHub (opens new window)

zhengwenfeng

穷则变,变则通,通则久
首页
  • Go语言高性能编程
分类
标签
归档
关于
  • 导航 (opens new window)
  • 代码片段 (opens new window)
  • 收藏
  • 友链
  • 外部页面

    • 开往 (opens new window)
GitHub (opens new window)
  • python

  • go语言

    • go简单使用grpc
    • gin中validator模块的源码分析
    • 优化gin表单的错误提示信息
    • go中如何处理error
    • tcp缓存引起的日志丢失
    • 使用etcd分布式锁导致的协程泄露与死锁问题
    • go语言高性能编程

      • Go协程池深度解析:原理、实现与最佳实践
      • Go语言Interface Boxing原理与性能优化指南
      • Go语言遍历性能深度解析:从原理到优化实践
      • Go语言零拷贝技术完全指南
      • Go语言不可变数据共享:无锁并发编程实践
      • Go语言内存预分配完全指南
      • Go语言原子操作完全指南
      • Go语言堆栈分配与逃逸分析深度解析
        • 1. 前言
          • 1.1 为什么需要关注堆栈分配?
          • 1.2 什么是逃逸分析?
        • 2. 逃逸分析实战
          • 2.1 如何查看逃逸分析结果
          • 2.2常见的逃逸场景
        • 3. 堆与栈的性能基准测试
        • 4. 最佳实践
          • 4.1 优化栈分配场景
          • 4.2 不必强求栈分配的场景
        • 5. 总结
      • Go语言空结构体:零内存消耗的高效编程
      • Go语言结构体内存对齐完全指南
      • Go语言字符串拼接性能对比与优化指南
      • Go语言延迟初始化(Lazy Initialization)最佳实践
      • Go语言高效IO缓冲技术详解
  • linux

  • 其他

  • 编程
  • go语言
  • go语言高性能编程
zhengwenfeng
2025-06-14
目录

Go语言堆栈分配与逃逸分析深度解析

# 1. 前言

# 1.1 为什么需要关注堆栈分配?

在Go语言中,内存分配主要有两种方式:

  • 栈分配:轻量快速,函数结束时自动释放,不产生垃圾
  • 堆分配:需要垃圾回收(GC)参与,开销较大

关键区别:

  • 栈分配比堆分配快10-100倍
  • 栈分配不会增加GC压力
  • 堆分配的对象生命周期更长

# 1.2 什么是逃逸分析?

逃逸分析是Go编译器在编译时进行的一种优化技术,它会分析变量的生命周期和使用方式,自动决定将变量分配在栈上还是堆上。

# 2. 逃逸分析实战

# 2.1 如何查看逃逸分析结果

在下面代码中,将x的变量的地址返回出去了,这个时候会将x放到堆上。

func allocate() *int {
	x := 42
	return &x // x escapes to the heap
}

func main() {
	allocate()
}
1
2
3
4
5
6
7
8

通过添加参数-gcflags="-m"可以看到如下逃逸的信息

$ go build -gcflags="-m"  main.go
...
./main.go:4:2: moved to heap: x
1
2
3

# 2.2常见的逃逸场景

  1. 返回局部变量指针
func escape() *int {
    x := 10
    return &x // escapes
}
1
2
3
4
  1. 闭包使用局部变量
func closureEscape() func() int {
    x := 5
    return func() int { return x } // x escapes
}
1
2
3
4
  1. 接口类型转换
func toInterface(i int) interface{} {
    return i // escapes if type info needed at runtime
}
1
2
3
  1. 全局变量赋值
var global *int

func assignGlobal() {
    x := 7
    global = &x // escapes
}
1
2
3
4
5
6
  1. 大尺寸对象
func makeLargeSlice() []int {
    s := make([]int, 10000) // may escape due to size
    return s
}
1
2
3
4

# 3. 堆与栈的性能基准测试

type Data struct {
	A, B, C int
}

// 栈分配
func StackAlloc() Data {
    return Data{1, 2, 3} // stays on stack
}

// 堆分配
func HeapAlloc() *Data {
    return &Data{1, 2, 3} // escapes to heap
}

func BenchmarkStackAlloc(b *testing.B) {
    for b.Loop() {
        _ = StackAlloc()
    }
}

func BenchmarkHeapAlloc(b *testing.B) {
    for b.Loop() {
        _ = HeapAlloc()
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

通过运行会发现两者区别并不大,而且竟然没有堆的申请。这是因为编译器很聪明,它发现通过HeapAlloc返回的指针没有任何意义,所以也就把它放在了栈上。

$ go test -bench=. -benchmem .  
goos: darwin
goarch: arm64
pkg: main/demo
cpu: Apple M4 Pro
BenchmarkStackAlloc-12          1000000000               0.2373 ns/op          0 B/op          0 allocs/op
BenchmarkHeapAlloc-12           1000000000               0.2253 ns/op          0 B/op          0 allocs/op
PASS
ok      main/demo       1.176s
1
2
3
4
5
6
7
8
9

我需要强制让它分配到堆上,使用全局赋值,修改代码后如下

type Data struct {
	A, B, C int
}

var sink *Data

func HeapAllocEscape() {
	d := &Data{1, 2, 3}
	sink = d // d escapes to heap
}

func StackAlloc() Data {
	return Data{1, 2, 3} // stays on stack
}

func BenchmarkStackAlloc(b *testing.B) {
	for range b.N {
		_ = StackAlloc()
	}
}

func BenchmarkHeapAlloc(b *testing.B) {
	for range b.N {
		HeapAllocEscape()
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

运行结果如下,使用堆存储的开销:35倍的慢调用,24 字节的分配和 1 次垃圾回收的对象。

$ go test -bench=. -benchmem .  
goos: darwin
goarch: arm64
pkg: main/demo
cpu: Apple M4 Pro
BenchmarkStackAlloc-12          1000000000               0.2285 ns/op          0 B/op          0 allocs/op
BenchmarkHeapAlloc-12           147388731                8.117 ns/op          24 B/op          1 allocs/op
PASS
ok      main/demo       3.064s
1
2
3
4
5
6
7
8
9

# 4. 最佳实践

# 4.1 优化栈分配场景

  • GC 压力大的时候
  • 对于短期的小对象
  • 高频调用的函数内部

# 4.2 不必强求栈分配的场景

  • 工厂方法返回对象指针(Go惯用法)
  • 对象需要跨函数生命周期。
  • 不频繁创建的小对象
  • 优化会影响代码可读性时

# 5. 总结

  1. 逃逸分析是Go的重要优化手段,自动决定变量分配位置。
  2. 栈分配性能优势明显,适合短生命周期对象。
  3. 堆分配虽然较慢,但在某些场景下是必要且合理的。
  4. 优化时要平衡性能与代码质量,避免过度优化。
#go语言#go语言高性能编程
上次更新: 2025/06/14, 16:24:58
Go语言原子操作完全指南
Go语言空结构体:零内存消耗的高效编程

← Go语言原子操作完全指南 Go语言空结构体:零内存消耗的高效编程→

最近更新
01
Go语言高效IO缓冲技术详解
06-14
02
Go语言延迟初始化(Lazy Initialization)最佳实践
06-14
03
Go语言字符串拼接性能对比与优化指南
06-14
更多文章>
Theme by Vdoing | Copyright © 2022-2025 zhengwenfeng | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式