绝大多数时候我们不需要关注内存管理, go运行时会自行处理, 但是对于热点路径, 我们必须确保高效地内存分配来榨取每一点性能
就内存分配而言, 有两件事情可以帮助我们提升性能: 1. 减少分配, 如将fmt.Sprintf
改为fmt.Fprintf
来避免创建新的字符串或者尽量使用[]byte
而不是string
来重用 2. 尽量避免在堆上的分配, 分配在堆的内存会增加GC花费从而降低性能, 本文将试图对此继续分析
需要提醒的是, 一切的性能优化都必须以性能分析为起点, 绝对不能进行未测量的优化.
堆分配与栈分配花销对比
通常而言, 堆分配的花销是远比栈分配更昂贵的. 因此高效的内存分配一定要尽量降低不必要的堆分配1. 认真分析的话, 栈分配和堆分配的花销
栈分配的花销比较简单, 仅仅需要两条CPU指令: 一个是推入栈中(来分配), 一个是从栈中释放.
堆分配的花销则主要在于分配和GC: malloc需要寻找一个足够大的闲置空间来存储, 而GC也需要扫描堆来发现可回收的对象. 这两种操作的时间成本显然高于栈分配的花销
什么情况下会触发堆分配?
在很多其他语言里面, 一个值会被分配在栈还是堆是清晰的:对于JAVA来说, 所有的对象(object)都会被分配至堆, 所有的基础类型都会分配在栈. 对于C/C++来说, 使用new创建的值就会被分配在堆, 否则默认是栈. 然而在GO中, 事情会变得更加复杂点.
比方说, 对于下面的代码段, 你是否能确定它分配在栈还是堆?
type user struct {
name string
age int
}
...
u:=user{
name:"li",
age:15,
}
答案是, 你不能, 你需要更多的信息判断. 比方说如果是下面的完整代码, 那么u会被分配在栈
func test()user{
u:=user{
name:"li",
age:15,
}
return u
}
而如果是下面的代码, u就会被分配至堆
func test()*user{
u:=user{
name:"li",
age:15,
}
return &u
}
在GO中, 一个值是否被分配至堆还是栈, 取决于GO编译器对其做的逃逸分析结果. 如果一个变量的生命周期和大小可以在编译时确定, 那么该变量就会栈分配, 否则就会触发堆分配.
这样做的目的有两点:
- 降低心智负担. 如果需要自行决定是否分配至堆, 那么往往我们还需要考虑怎么释放堆资源. (也就是没有GC, 类似于C++和C)
- 降低GC负担, 因为逃逸分析可以避免将所有对象都分配至堆中.
尽管我们无法实际控制分配至堆还是栈, 但是我们仍然有必要了解, 什么情况下值会被分配至堆, 以及对此我们有什么绕过去的办法.
下面列举的是常见的堆分配的例子, 有一些可能意料之中, 而有一些也许会出乎意料:他们也是性能优化的目标
跨越栈帧(stack frame)的值传递
最为常见也为大家所理解的情况, 莫过于下面的类似例子
func New()*user{
u:=user{
name:"li",