59

Go: 理解 Sync.Pool 的设计

 5 years ago
source link: https://www.tuicool.com/articles/VB3aYvB
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

qYveMju.jpg!web

:information_source:本文基于 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 之间)。 文档 也告知我们:

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

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

QJRFNfZ.jpg!web

对于我们创建的每个 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 访问的新流程:

A73Q7n2.jpg!web

使用这个新的链式结构池,每个处理器可以在其 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 缓存。


Recommend

  • 69
    • studygolang.com 5 years ago
    • Cache

    GO: sync.Pool 的实现与演进

    上一次写了 sync.Mutex 的实现与演进后,感觉有必要对 GO 标准库的一些功能进行追踪,看看不断优化的过程,发掘有意思的点。一般 sync.Pool 用作小对像池,比如前公司同事,在...

  • 69
    • www.tuicool.com 5 years ago
    • Cache

    我所理解的Sync Pool

    看gin源码时发现了 sync.Pool 的使用 // gin.go:L144 func New() *Engine { ... engine.pool.New = func() interface{} { return engine.allocateContext() } return engine } // gin...

  • 55
    • www.tuicool.com 5 years ago
    • Cache

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

    原文地址: medium.com/@blanchon.v… 原文作者:

  • 57
    • www.tuicool.com 5 years ago
    • Cache

    golang sync.Pool 分析

    在 echo 官网的手册 上可以看到 echo 框架的路由性能主要依赖于...

  • 115
    • www.tuicool.com 5 years ago
    • Cache

    Go 1.13中 sync.Pool 是如何优化的?

    Go 1.13持续对 sync.Pool 进行了改进,这里我们有两个简单的灵魂拷问: 做了哪些改进? 如何做的改进? 首先回答第一个问题: 对STW暂停时间做了优化, 避免大的sy...

  • 43
    • studygolang.com 5 years ago
    • Cache

    golang中神奇的sync.Pool

    前言 在 golang 中有一个池,它特别神奇,你只要和它有个约定,你要什么它就给什么,你用完了还可以还回去,但是下次拿的时候呢,确不一定是你上次存的那个,这个池就是 sync.Pool 说实话第一次看到这个东西的时候,...

  • 30
    • studygolang.com 5 years ago
    • Cache

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

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

  • 27
    • studygolang.com 5 years ago
    • Cache

    Go语言学习 - Sync.Pool

    直接说吧, 这个东西为什么存在, 为了解决什么问题: 假设我们需要频繁申请内存用于存放你的结构体, 而这个结构体本身是短命的, 可能这个请求过去你就不用了. 申请了这么多内存, 对于GC来说就是一种压力了. 针对这个问题, 如果我们能...

  • 18
    • studygolang.com 5 years ago
    • Cache

    Go Sync.Pool作用及遇到的坑

    Go版本1.13.1 Go中有sync.Pool类型,我们可以把它理解成存放临时值的容器,之所以加上“临时”两个字,是因为它会在GC过程的STW步骤被清理。 sync.Pool类型使用前可以给它的New字段赋值,New字段类型是func() interface{},一...

  • 26
    • studygolang.com 4 years ago
    • Cache

    sync pool中的几个问题解答

    之前在分享中,发现了自己在看sync.pool的源码中忽略了两个个重要的问题。这两个问题,确实是在看源码的过程中自己没思考的。确实,这也是分享的好处,分享让自己能从不同角度重新看待原来的问题。废话不过说,下面上课。 nocop...

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK