日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

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

發布時間:2024/4/15 编程问答 40 豆豆
生活随笔 收集整理的這篇文章主要介紹了 [译] Go: 理解 Sync.Pool 的设计 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
  • 原文地址:medium.com/@blanchon.v…
  • 原文作者:Vincent Blanchon
  • 譯文地址:github.com/watermelo/d…
  • 譯者:咔嘰咔嘰
  • 譯者水平有限,如有翻譯或理解謬誤,煩請幫忙指出

??本文基于 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 *Smallfor 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 *Smallfor i := 0; i < b.N; i++ {for j := 0; j < 10000; j++ {s = pool.Get().(*Small)s.a = 1b.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) } 復制代碼

他將注冊一個在運行時清理 pool 對象的方法。GC 在文件 runtime/mgc.go 中將觸發這個方法:

func gcStart(trigger gcTrigger) {[...]// 在開始 GC 前調用 clearpoolsclearpools() 復制代碼

這就解釋了為什么在調用 GC 時性能較低。因為每次 GC 運行時都會清理 pool 對象(譯者注:pool 對象的生存時間介于兩次 GC 之間)。文檔也告知我們:

存儲在池中的任何內容都可以在不被通知的情況下隨時自動刪除

現在,讓我們創建一個流程圖以了解池的管理方式:

對于我們創建的每個 sync.Pool,go 生成一個連接到每個處理器(譯者注:處理器即 Go 中調度模型 GMP 的 P,pool 里實際存儲形式是 [P]poolLocal)的內部池 poolLocal。該結構由兩個屬性組成:private 和 shared。第一個只能由其所有者訪問(push 和 pop 不需要任何鎖),而 shared 屬性可由任何其他處理器讀取,并且需要并發安全。實際上,池不是簡單的本地緩存,它可以被我們的應用程序中的任何 線程/goroutines 使用。

Go 的 1.13 版本將改進 shared 的訪問,并且還將帶來一個新的緩存,以解決 GC 和池清理相關的問題。

新的無鎖池和 victim 緩存

Go 1.13 版將 shared 用一個雙向鏈表poolChain作為儲存結構,這次改動刪除了鎖并改善了 shared 的訪問。以下是 shared 訪問的新流程:

使用這個新的鏈式結構池,每個處理器可以在其 shared 隊列的頭部 push 和 pop,而其他處理器訪問 shared 只能從尾部 pop。由于 next/prev 屬性,shared 隊列的頭部可以通過分配一個兩倍大的新結構來擴容,該結構將鏈接到前一個結構。初始結構的默認大小為 8。這意味著第二個結構將是 16,第三個結構 32,依此類推。

此外,現在 poolLocal 結構不需要鎖了,代碼可以依賴于原子操作。

關于新加的 victim 緩存(譯者注:關于引入 victim 緩存的 commit,引入該緩存就是為了解決之前 Benchmark 那個問題),新策略非常簡單。現在有兩組池:活動池和存檔池(譯者注:allPools 和 oldPools)。當 GC 運行時,它會將每個池的引用保存到池中的新屬性(victim),然后在清理當前池之前將該組池變成存檔池:

// 從所有 pool 中刪除 victim 緩存 for _, p := range oldPools {p.victim = nilp.victimSize = 0 }// 把主緩存移到 victim 緩存 for _, p := range allPools {p.victim = p.localp.victimSize = p.localSizep.local = nilp.localSize = 0 }// 非空主緩存的池現在具有非空的 victim 緩存,并且池的主緩存被清除 oldPools, allPools = allPools, nil 復制代碼

有了這個策略,應用程序現在將有一個循環的 GC 來 創建/收集 具有備份的新元素,這要歸功于 victim 緩存。在之前的流程圖中,將在請求"shared" pool 的流程之后請求 victim 緩存。

轉載于:https://juejin.im/post/5d006254e51d45776031afe3

總結

以上是生活随笔為你收集整理的[译] Go: 理解 Sync.Pool 的设计的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。