[译]Go: 理解Sync.Pool的设计思想

野生程序元 · · 156 次点击 · · 开始浏览    

原文:medium.com/a-journey-w…

这篇文章基于Go1.12和1.13,我们来看看这两个版本间sync/pool.go的革命性变化。

Sync包提供了强大的可被重复利用实例池,为了降低垃圾回收的压力。在使用这个包之前,需要将你的应用跑出使用pool之前与之后的benchmark数据,因为在一些情况下使用如果你不清楚pool内部原理的话,反而会让应用的性能下降。

pool的局限性

我们先来看看一些基础的例子,来看看他在一个相当简单情况下(分配1K内存)是如何工作的:

type Small struct {
   a int
}

var pool = sync.Pool{
   New: func() interface{} { return new(Small) },
}

//go:noinline
func inc(s *Small) { s.a++ }

func BenchmarkWithoutPool(b *testing.B) {
   var s *Small
   for i := 0; i < b.N; i++ {
      for j := 0; j < 10000; j++ {
         s = &Small{ a: 1, }
         b.StopTimer(); inc(s); b.StartTimer()
      }
   }
}

func BenchmarkWithPool(b *testing.B) {
   var s *Small
   for i := 0; i < b.N; i++ {
      for j := 0; j < 10000; j++ {
         s = pool.Get().(*Small)
         s.a = 1
         b.StopTimer(); inc(s); b.StartTimer()
         pool.Put(s)
      }
   }
}
复制代码

下面是两个benchmarks,一个是使用了sync.pool一个没有使用

name           time/op        alloc/op        allocs/op
WithoutPool-8  3.02ms ± 1%    160kB ± 0%      1.05kB ± 1%
WithPool-8     1.36ms ± 6%   1.05kB ± 0%        3.00 ± 0%
复制代码

由于这个遍历有10k的迭代,那个没有使用pool的benchmark显示在堆上创建了10k的内存分配,而使用了pool的只使用了3. 3个分配由pool进行的,但只有一个结构体的实例被分配到内存。到目前为止可以看到使用pool对于内存的处理以及内存消耗上面更加友善。

但是,在实际例子里面,当你使用pool,你的应用将会有很多新在堆上的内存分配。这种情况下,当内存升高了,就会触发垃圾回收。

我们可以强制垃圾回收的发生通过使用runtime.GC()来模拟这种情形

name           time/op        alloc/op        allocs/op
WithoutPool-8  993ms ± 1%    249kB ± 2%      10.9k ± 0%
WithPool-8     1.03s ± 4%    10.6MB ± 0%     31.0k ± 0%
复制代码

我们现在可以看到使用了pool的情况反而内存分配比不使用pool的时候高了。我们来深入地看一下这个包的源码来理解为什么会这样。

内部工作流

看一下sync/pool.go文件会给我们展示一个初始化函数,这个函数里面的内容能解释我们刚刚的情景:

func init() {
   runtime_registerPoolCleanup(poolCleanup)
}
复制代码

这里在运行时注册成了一个方法去清理pools。并且同样的方法在垃圾回收里面也会触发,在文件runtime/mgc.go里面

func gcStart(trigger gcTrigger) {
   [...]
   // clearpools before we start the GC
   clearpools()
复制代码

这就解释了为什么当调用垃圾回收时,性能会下降。pools在每次垃圾回收启动时都会被清理。这个文档其实已经有警告我们

Any item stored in the Pool may be removed automatically at any time without notification
复制代码

接下来让我们创建一个工作流来理解一下这里面是如何管理的

sync.Pool workflow in Go 1.12

我们创建的每一个sync.Pool,go都会生成一个内部池poolLocal连接着各个processer(GMP中的P)。这些内部的池由两个属性组成privateshared。前者只是他的所有者可以访问(push以及pop操作,也因此不需要锁),而`shared可以被任何processer读取并且是需要自己维持并发安全。而实际上,pool不是一个简单的本地缓存,他有可能在我们的程序中被用于任何的协程或者goroutines

Go的1.13版将改善对shared的访问,还将带来一个新的缓存,该缓存解决与垃圾回收器和清除池有关的问题。

新的无需锁pool和victim cache

Go 1.13版本使用了一个新的双向链表作为shared pool,去除了锁,提高了shared的访问效率。这个改造主要是为了提高缓存性能。这里是一个访问shared的流程

new shared pools in Go 1.13

在这个新的链式pool里面,每一个processpr都可以在链表的头进行push与pop,然后访问shared可以从链表的尾pop出子块。结构体的大小在扩容的时候会变成原来的两倍,然后结构体之间使用next/prev指针进行连接。结构体默认大小是可以放下8个子项。这意味着第二个结构体可以容纳16个子项,第三个是32个子项以此类推。同样地,我们现在不再需要锁,代码执行具有原子性。

关于新缓存,新策略非常简单。 现在有2组池:活动池和已归档池。 当垃圾收集器运行时,它将保留每个池对该池内新属性的引用,然后在清理当前池之前将池的集合复制到归档池中:

// Drop victim caches from all pools.
for _, p := range oldPools {
   p.victim = nil
   p.victimSize = 0
}

// Move primary cache to victim cache.
for _, p := range allPools {
   p.victim = p.local
   p.victimSize = p.localSize
   p.local = nil
   p.localSize = 0
}

// The pools with non-empty primary caches now have non-empty
// victim caches and no pools have primary caches.
oldPools, allPools = allPools, nil
复制代码

通过这种策略,由于受害者缓存,该应用程序现在将有一个更多的垃圾收集器周期来创建/收集带有备份的新项目。 在工作流中,将在共享池之后在过程结束时请求牺牲者缓存。

本文来自:掘金

感谢作者:野生程序元

查看原文:[译]Go: 理解Sync.Pool的设计思想

入群交流(和以上内容无关):加入Go大咖交流群,免费领全套学习资料或添加微信:muxilin131420 备注:入群;或加QQ群:729884609

156 次点击  ∙  1 赞  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传