前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Go语言中常见100问题-#96 Not knowing how to reduce allocations

Go语言中常见100问题-#96 Not knowing how to reduce allocations

作者头像
数据小冰
发布2024-02-01 17:04:04
1070
发布2024-02-01 17:04:04
举报
文章被收录于专栏:数据小冰数据小冰
不懂如何减少内存分配

减少内存分配是Go应用程序的一个常见优化事项。本系列文章已介绍了不少减少堆上内存分配的方法:

本文将讨论三种减少内存分配的常用方法:

  • 调整API
  • 编译器优化
  • 使用sync.Pool
调整API

提供良好的API接口,下面看一个具体例子,即 io.Reader接口。

代码语言:javascript
复制
type Reader interface {
 Read(p []byte) (n int, err error)
}

Read方法接收一个切片并返回读取到的字节数。假如 io.Reader接口反过来设计:传递一个表示需要读取多少字节的参数int并返回一个切片。代码如下,从语义上来说,没有问题。但是在这种情况下,返回的切片会自动逃逸到堆中。如果由调用者提供切片,很有可能该切片在栈上分配,但不一定意味着它不会逃逸:具体情况编译器会判断。通过这种很小的变化,将决定权交给调用者,而不是由调用的 Read 方法来约束。有时这种微小的API变化也会对内存分配产生良好的影响。

编译器优化

Go编译器的目标之一是尽可能优化我们编写的代码,通过下面map例子说明。在Go语言中,不能使用切片作为map的key。在某些情况下,特别是涉及I/O的应用中,可能想将收到的[]byte数据作为key, 这时必须先将它转成一个字符串。具体代码如下:

代码语言:javascript
复制
type cache struct {
 m map[string]int
}

func (c *cache) get(bytes []byte) (v int, contains bool) {
 key := string(bytes)
 v, contains = c.m[key]
 return
}

然而,如果直接使用 string(bytes) 在map中查询,Go语言编译器会实现一个特定的优化:

代码语言:javascript
复制
func (c *cache) get(bytes []byte) (v int, contains bool) {
 v, contains = c.m[string(bytes)]
 return
}

通过小小调整,编译器会避免进行字节到字符串的转换,所以第二个版本比第一个要快。此外,通过上面的例子表明相似的代码可能导致Go编译器生成不同的汇编代码。我们应该多多了解类似的编译器优化,编写性能更好的程序。与此同时也要关注Go新版本,了解最新优化特性。

sync.Pool

如果想减少内存中对象的分配数量,一种处理方法是使用 sync.Pool. 注意 sync.Pool 不能当做缓存理解:没有可以设置的固定大小和最大容量。应该把它理解为一个重复使用的公共对象的池。假设想要实现一个 write 函数,接收一个 io.Writer入参,调用 getResponse 函数获得一个 []byte 切片,然后将切片数据写入到 io.Writer。实现代码如下(为了逻辑清晰,省略了错误处理)。

代码语言:javascript
复制
func write(w io.Writer) {
 b := getResponse()
 _, _ = w.Write(b)
}

每次调用 getResponse 函数都会返回一个新的 byte切片。如果想复用切片以达到减少切片分配该怎么做呢?假设getResponse函数返回的切片最大长度为 1024字节。这种情况下,我们可以使用大杀器 sync.Pool.

创建一个 sync.Pool 对象需要提供一个用于初始化的工厂函数: func() any。实例代码如下,这里提供的初始化函数会返回一个长度为1024字节的切片。在 write 函数中,尝试从sync.Pool对象池中获取一个字节切片,如果对象池为空,则调用New函数创建一个新的切片,否则会直接从对象池中取。注意关键一步操作是对 buffer = buffer[:0],因为获取的buffer存在之前残留的数据,所以需要重置掉。使用完 buffer后,调用 pool.Put将buffer归还到对象池。

代码语言:javascript
复制
var pool = sync.Pool{
 New: func() any {
  return make([]byte, 1024)
 },
}

func write(w io.Writer) {
 buffer := pool.Get().([]byte)
 buffer = buffer[:0] // 重置缓冲区
 defer pool.Put(buffer)

 getResponse(buffer)
 _, _ = w.Write(buffer)
}

sync.Pool对象对外提供两个方法:

  • Get() any : 从对象池中返回一个对象
  • Put(any) : 将用完的对象归还到对象池中

如上图所示,Get方法会从池子中拿一个对象给调用方,如果池子是空的,则调用New函数创建一个新的对象。使用完对象后,可以调用 Put 方法将对象归还到池子中。池子中的对象什么时候会被销毁呢?我们无法控制,完全由系统GC说了算,在两轮GC后对象会被系统回收。

可以看到第二版本的 write 函数可以减少[]byte创建,如果对象池中有直接取用,整体上会减少创建切片成本开销。在需要频繁分配对象场合,可以考虑使用sync.Pool,利用临时对象可以避免重复分配同类型的数据,并且sync.Pool可供多个goroutine同时使用,是并发安全的。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2024-01-29,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 数据小冰 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 不懂如何减少内存分配
    • 调整API
      • 编译器优化
        • sync.Pool
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档