避免频繁分配相同类型临时对象的开销
问题 : 不停地创建临时对象,Golang 运行时的哪些操作会消耗 CPU 资源?
首先是内存分配。我们不停地创建对象时,就得不断地在堆里面找空闲的内存块,然后进行分配。这就像是在一个大仓库里,每次创建新对象都要重新找地方放,这个过程是很消耗资源的.
垃圾收集(GC)。要是临时对象很多,那在进行垃圾收集的时候,就需要耗费更多的 CPU 资源去扫描这些对象,看看哪些没用了,然后清理掉。这就像大扫除的时候,如果杂物太多,打扫起来就会更费劲。
问题: 如何避免频繁创建临时对象而产生的内存分配和 GC 开销?
由于我们每次申请的都是同一种类型的对象,也就是 bytes.Buffer。既然这样,像下面的图一样,我们可以在每次用完这个临时对象之后,把它放到一个池子里。等下次要用的时候,就直接从池子里取出来用。这样一来,就不用每次都从堆里重新分配,而且 GC 需要扫描的临时对象也会减少,这就是对象池的思想。
GO 如何创建对象池?对于对象池,Golang 里有个 sync.Pool 类型来支持
Go
var objectPool = sync.Pool{
New: func() interface{} {
return &bytes.Buffer{}
},
}
func WriteBufferWithPool() {
// 获取临时对象
buf := objectPool.Get().(*bytes.Buffer)
// 使用
buf.Write(data)
// 将buf对象里面的字段恢复初始值
buf.Reset()
// 放回
objectPool.Put(buf)
}
从 CPU 资源消耗来看,不使用对象池,单次函数调用要 232.2ns,而使用对象池的方式,只要 32.81ns,节约了 86% 左右的 CPU 资源。
从内存消耗来看,使用对象池的方式,平均单次请求从堆中分配的大小和次数直接变成了 0。
问题:协程池:如何避免频繁创建协程的开销?
第一就是内存分配。在 Golang 中,每次创建一个协程,默认都得分配 2KB 的内存空间,而且还要进行初始化。要是不停地创建协程,那就得不停地进行内存分配和初始化,这个过程就需要消耗不少 CPU 资源。
第二就是协程调度。特别是那种 CPU 密集型的应用,如果频繁创建协程,将会导致处于就绪状态、能被 CPU 调度的协程过多。协程调度就会变得很频繁,要知道,协程调度这个过程本身也是会消耗 CPU 资源的。