[Go語言]binary tree算法的華山論劍

前言

  在benchmarkgame(世界上最火的性能對比網站)上,Go語言一直有一個槽點,就是極其慢的binary tree性能,執行用時40秒 (我的機器上,16秒),與此對比,Java版本是6秒,那麼問題來了:爲什麼慢得令人髮指?我們來深入研究下慢的原因,然後看看能否對其進行改進。

  對於binary tree算法中,最耗性能的地方就是海量的node分配和bottomUpTree()遞歸函數的調用,與這兩項對應的go的特性就是gc的goroutine的堆棧分配。

GC

這個世界沒有完美的GC,任何選擇都有代價,一般來說就是低延遲和高吞吐的權衡。


問題描述

   Java採用的是分代GC,優點是node的分配非常輕量,缺點就是分代gc需要使用更多的內存空間,同時當對象被移動到tenured堆時,會發生大量的內存拷貝。

  Go的gc不是分代的,因此在node的分配上需要消耗更多的資源。Go的gc選擇了超低的延遲,同時犧牲了部分吞吐,對於絕大多數應用來說,Go的選擇是非常正確的,但是對於binary tree這種算法來說,就不是很適合了。

解決方案

  針對GC的優化,有兩個通用的解決方案就是提前分配合適的堆棧空間和對象複用。對於binary tree算法,就是Nodes的預先分配和複用。


Goroutine的堆棧

輕量的Goroutine是Go語言的靈魂所在


問題描述

  爲了讓goroutine儘可能輕量,go僅僅爲每個goroutine分配了2KB的初始堆棧大小,在之後Go會根據需要動態的擴展堆棧大小。同樣,對於絕大多數場景,這種選擇都是非常正確的,但是針對binary tree算法,這種選擇就有了一些問題。

  Go是在每次函數調用之前檢查goroutine的堆棧大小,如果發現當前堆棧不夠用,就會重新分配一個新的堆棧空間,然後將舊的堆棧拷貝到新的裏。這種操作開銷是很小的,但是在binary tree中,bottomUpTree()基本上不做什麼工作,調用卻是極其頻繁,這樣一來再小的開銷累積起來也會非常可觀。而且這個函數的調用是深遞歸,當堆棧需要增長時,可能會拷貝幾次,不僅僅是一次!

解決方案

  將bottomUpTree()改爲非遞歸的函數,雖然不易實現,但是還是可以做到的。


新舊Binary tree實現對比

沒有對比,就沒有傷害!


舊版本代碼鏈接

運行用時:

 > time go run old.go 20
stretch tree of depth 21     check: -1
2097152  trees of depth 4    check: -2097152
524288   trees of depth 6    check: -524288
131072   trees of depth 8    check: -131072
32768    trees of depth 10   check: -32768
8192     trees of depth 12   check: -8192
2048     trees of depth 14   check: -2048
512  trees of depth 16   check: -512
128  trees of depth 18   check: -128
32   trees of depth 20   check: -32
long lived tree of depth 20  check: -1

real    0m16.279s
user    1m47.569s
sys 0m2.663s

新版本代碼鏈接

運行用時

time go run new.go 20
stretch tree of depth 21     check: -1
2097152  trees of depth 4    check: -2097152
524288   trees of depth 6    check: -524288
131072   trees of depth 8    check: -131072
32768    trees of depth 10   check: -32768
8192     trees of depth 12   check: -8192
2048     trees of depth 14   check: -2048
512  trees of depth 16   check: -512
128  trees of depth 18   check: -128
32   trees of depth 20   check: -32
long lived tree of depth 20  check: -1
dur: 1.71074946s

real    0m1.914s
user    0m10.149s
sys 0m0.157s


結論

  性能從16.28秒提升到了1.91秒,提升巨大!

  這裏提出的解決方案看似是針對binary tree,其實對於任何GC語言和使用場景來說都是通用的。

  牢記這兩種解決方案吧:

  • 內存空間預分配
  • 對象複用
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章