【go代码优化】函数返回值类型还是指针?

二维码
| Sep 07, 2020 | 原创

关于这个话题,一般的思维是返回指针类型会比返回值类型数据更节约内存,确实没错,但是在 go 语言里函数返回指针类型的数据,常常会引发另一个问题:内存逃逸分析 知乎上这篇文章描述的已经很到位。本文主要是基于自己的项目实践,测试一下函数返回不同数据类型的性能到底如何。

通常使用 go 作为 web 项目避免不了数据库的大数据内容作为函数返回的,正好我们可以来测试下返回指针类型和值类型的数据性能差异如何。

// 定义一个测试数据 model
type Model struct {
    Id           uint64
    Name         string
    Summary      string
    CreatedAt    time.Time
    Status       int
}

func GetM() Model {
    return Model{
	Id:        1,
	Name:      "media",
	Summary:   "How to improve your GO code with empty structs",
	CreatedAt: time.Now(),
	Status:    1,
    }
}

func GetMP() *Model {
    return &Model{
	Id:        1,
	Name:      "media",
	Summary:   "How to improve your GO code with empty structs",
	CreatedAt: time.Now(),
	Status:    1,
    }
}

这里定义一个测试的 gorm model,然后我们封装两个函数,分别返回值类型和指针类型,下面我们编写 benchmark 基准测试用例:

func BenchmarkFuncValue(b *testing.B) {
    var p Model
    for i := 0; i < b.N; i++ {
	p = GetM()
    }

    _ = fmt.Sprintf("%d", p.Id)
}

func BenchmarkFuncPointer(b *testing.B) {
    var p *Model
    for i := 0; i < b.N; i++ {
	p = GetMP()
    }

    if p != nil {
	_ = fmt.Sprintf("%d", p.Id)
    }
}

测试代码也非常简单,函数返回值赋值给外部变量 p,其目的用以模拟内存逃逸现象,不过函数返回值赋于外部变量其实也是我们日常的操作行为,最终测试效果如下:

go test -bench=Func -benchmem
goos: darwin
goarch: amd64
pkg: test
BenchmarkFuncValue-4            12708853       88.2 ns/op       0 B/op      0 allocs/op
BenchmarkFuncPointer-4           7094581       156 ns/op      208 B/op      1 allocs/op

从结果上来看,你会发觉 返回指针类型数据反而比反正值类型慢了接近1倍,并且还产生了一次内存分配的操作

如果你有兴趣,可以深入了解下关于内存逃逸分析的相关话题,这里借鉴下文章开头的引用文章结论:

  1. 堆上动态分配内存比栈上静态分配内存,开销大很多。
  2. 变量分配在栈上需要能在编译期确定它的作用域,否则会分配到堆上。
  3. Go编译器会在编译期对考察变量的作用域,并作一系列检查,如果它的作用域在运行期间对编译器一直是可知的,那么就会分配到栈上。简单来说,编译器会根据变量是否被外部引用来决定是否逃逸。
  4. 对于Go程序员来说,编译器的这些逃逸分析规则不需要掌握,我们只需通过go build -gcflags ‘-m’命令来观察变量逃逸情况就行了。
  5. 不要盲目使用变量的指针作为函数参数,虽然它会减少复制操作。但其实当参数为变量自身的时候,复制是在栈上完成的操作,开销远比变量逃逸后动态地在堆上分配内存少的多。
  6. 逃逸分析在编译阶段完成的。