Go: 理解 Sync.Pool 的设计

watermelo · 2019-06-30 22:54:33 · 3250 次点击 · 预计阅读时间 5 分钟 · 大约8小时之前 开始浏览    
这是一个创建于 2019-06-30 22:54:33 的文章,其中的信息可能已经有所发展或是发生改变。

sync pool

ℹ️本文基于 Go 1.12 和 1.13 版本,并解释了这两个版本之间 sync/pool.go 的演变。

sync 包提供了一个强大且可复用的实例池,以减少 GC 压力。在使用该包之前,我们需要在使用池之前和之后对应用程序进行基准测试。这非常重要,因为如果不了解它内部的工作原理,可能会影响性能。

池的限制

我们来看一个例子以了解它如何在一个非常简单的上下文中分配 10k 次:

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)
      }
   }
}

上面有两个基准测试,一个没有使用 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 次迭代,因此不使用池的基准测试在堆上需要 10k 次内存分配,而使用了池的基准测试仅进行了 3 次分配。 这 3 次分配由池产生的,但却只分配了一个结构实例。目前看起来还不错;使用 sync.Pool 更快,消耗更少的内存。

但是,在一个真实的应用程序中,你的实例可能会被用于处理繁重的任务,并会做很多堆内存分配。在这种情况下,当内存增加时,将会触发 GC。我们还可以使用命令 runtime.GC() 来强制执行基准测试中的 GC 来模拟此行为:(译者注:可以在 Benchmark 的每次迭代中添加 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%

我们现在可以看到,在 GC 的情况下池的性能较低,分配数和内存使用也更高。我们继续更深入地了解原因。

池的内部工作流程

深入了解 sync/pool.go 包的初始化,可以帮助我们回答之前的问题:

func init() {
   runtime_registerPoolCleanup(poolCleanup)
}

他将注册到 runtime 作为一个方法去清理池。GC 在文件 runtime/mgc.go 中将触发这个方法:

func gcStart(trigger gcTrigger) {
   [...]
   // 在 GC 之前调用 clearpools
   clearpools()

这就解释了为什么在调用 GC 时性能较低。因为每次 GC 运行时都会清理池对象(译者注:池对象的生存时间介于两次 GC 之间)。文档 也告知我们:

存储在池中的任何内容都可以在不被通知的情况下随时自动删除

现在,让我们创建一个流程图以了解池的管理方式:

sync.Pool workflow in Go 1.12

对于我们创建的每个 sync.Pool,go 生成一个连接到每个处理器 ( 译者注:处理器即 Go 中调度模型 GMP 的 P,pool 里实际存储形式是 [P]poolLocal) 的内部池 poolLocal。该结构由两个属性组成:privateshared。第一个只能由其所有者访问(push 和 pop 不需要任何锁),而 shared 属性可由任何其他处理器读取,并且需要并发安全。实际上,池不是简单的本地缓存,它可以被我们的应用程序中的任何 线程 /goroutines 使用。

Go 的 1.13 版本将改进 shared 的访问,并且还将带来一个新的缓存,以解决 GC 和池清理相关的问题。

新的无锁池和 victim 缓存

Go 1.13 版将 shared 用一个双向链表poolChain 作为储存结构,这次改动删除了锁并改善了 shared 的访问。以下是 shared 访问的新流程:

new shared pools in Go 1.13

使用这个新的链式结构池,每个处理器可以在其 shared 队列的头部 push 和 pop,而其他处理器访问 shared 只能从尾部 pop。由于 next/prev 属性,shared 队列的头部可以通过分配一个两倍大的新结构来扩容,该结构将链接到前一个结构。初始结构的默认大小为 8。这意味着第二个结构将是 16,第三个结构 32,依此类推。

此外,现在 poolLocal 结构不需要锁了,代码可以依赖于原子操作。

关于新加的 victim 缓存(译者注:关于引入 victim 缓存的 commit,所谓受害者缓存 Victim Cache,是一个与直接匹配或低相联缓存并用的、容量很小的全相联缓存。当一个数据块被逐出缓存时,并不直接丢弃,而是暂先进入受害者缓存。如果受害者缓存已满,就替换掉其中一项。当进行缓存标签匹配时,在与索引指向标签匹配的同时,并行查看受害者缓存,如果在受害者缓存发现匹配,就将其此数据块与缓存中的不匹配数据块做交换,同时返回给处理器。),新策略非常简单。现在有两组池:活动池和存档池(译者注:allPoolsoldPools)。当 GC 运行时,它会将每个池的引用保存到池中的新属性(victim),然后在清理当前池之前将该组池变成存档池:

// 从所有 pool 中删除 victim 缓存
for _, p := range oldPools {
   p.victim = nil
   p.victimSize = 0
}

// 把主缓存移到 victim 缓存
for _, p := range allPools {
   p.victim = p.local
   p.victimSize = p.localSize
   p.local = nil
   p.localSize = 0
}

// 非空主缓存的池现在具有非空的 victim 缓存,并且池的主缓存被清除
oldPools, allPools = allPools, nil

有了这个策略,应用程序现在将有一个循环的 GC 来 创建 / 收集 具有备份的新元素,这要归功于 victim 缓存。在之前的流程图中,将在请求 "shared" pool 的流程之后请求 victim 缓存。


via: https://medium.com/@blanchon.vincent/go-understand-the-design-of-sync-pool-2dde3024e277

作者:Vincent Blanchon  译者:watermelo  校对:polaris1119

本文由 GCTT 原创编译,Go语言中文网 荣誉推出


有疑问加站长微信联系(非本文作者))

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

3250 次点击  ∙  1 赞  
加入收藏 微博
被以下专栏收入,发现更多相似内容
3 回复  |  直到 2019-07-09 17:01:08
LSivan
LSivan · #1 · 6年之前

有点尴尬,直接拷的代码,跑了下发现结果不一样

代码

package pool

import (
    "runtime"
    "sync"
    "testing"
)

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 < 100000; j++ {
            s = &Small{ a: 1, }
            b.StopTimer(); inc(s); b.StartTimer()
        }
        runtime.GC()
    }

}

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

结果

BenchmarkWithoutPool-8                30          42271560 ns/op         1600078 B/op     100000 allocs/op
BenchmarkWithPool-8                  100          20146449 ns/op            1090 B/op          5 allocs/op

1.12.6的Go,有趣有趣,姿势不正确还是不一定国外文章都对?

watermelo
watermelo · #2 · 6年之前

,。,

watermelo
watermelo · #3 · 6年之前
LSivanLSivan #1 回复

有点尴尬,直接拷的代码,跑了下发现结果不一样 代码 ```golang package pool import ( "runtime" "sync" "testing" ) 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 < 100000; j++ { s = &Small{ a: 1, } b.StopTimer(); inc(s); b.StartTimer() } runtime.GC() } } func BenchmarkWithPool(b *testing.B) { var s *Small for i := 0; i < b.N; i++ { for j := 0; j < 100000; j++ { s = pool.Get().(*Small) s.a = 1 b.StopTimer(); inc(s); b.StartTimer() pool.Put(s) } runtime.GC() } } ``` 结果 ```bash BenchmarkWithoutPool-8 30 42271560 ns/op 1600078 B/op 100000 allocs/op BenchmarkWithPool-8 100 20146449 ns/op 1090 B/op 5 allocs/op ``` 1.12.6的Go,有趣有趣,姿势不正确还是不一定国外文章都对?

大致比例是差不多的,也能看出WithPool能带来的提升,作者的那几个数据的单位他自己换算过可能单位存在一些错误。 这是这次跑的数据,跟你的也差不多。

goos: darwin
goarch: amd64
pkg: pooltest
BenchmarkWithoutPool-4                20          76755420 ns/op         1600004 B/op     100000 allocs/op
BenchmarkWithPool-4                   50          29498700 ns/op             600 B/op          4 allocs/op

和文章对比也就是

name           time/op        alloc/op        allocs/op
WithoutPool-8  3.02ms ± 1% (76.7ms)    160kB ± 0% (1600kB)     1.05kB ± 1% 
WithPool-8     1.36ms ± 6%(29.5ms)   1.05kB ± 0%  (0.6KB)      3.00 ± 0%

每次执行时间比 2:1,分配内存比1600:1,分配次数比 100000:4。

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