Go 正在構建的 GC 不僅僅是面向于 2015 年,而是面向于 2025 年甚至更久之后:一個支持當局的軟件開發(fā)并且可以同未來十年新的軟件硬件一同擴展的 GC。
創(chuàng)新互聯(lián)長期為上千多家客戶提供的網(wǎng)站建設服務,團隊從業(yè)經(jīng)驗10年,關注不同地域、不同群體,并針對不同對象提供差異化的產(chǎn)品和服務;打造開放共贏平臺,與合作伙伴共同營造健康的互聯(lián)網(wǎng)生態(tài)環(huán)境。為河北企業(yè)提供專業(yè)的做網(wǎng)站、成都做網(wǎng)站,河北網(wǎng)站改版等技術服務。擁有十余年豐富建站經(jīng)驗和眾多成功案例,為您定制開發(fā)。
Go 1.5,是看到未來的第一瞥, 成功完成了我們一年前設定的 GC 延遲小于 10ms 的目標 。這篇文章綜述了我們在 Go 1.5 的垃圾回收器上做了什么。
Go 的新 GC 是一個 并發(fā)的、三色的、標記清除的垃圾回收器(a concurrent, tri-color, mark-sweep collector)。我們相信它會非常適合于新時代硬件的性能和新時代軟件的低延遲需求。
這些步驟都(?)發(fā)生在與應用程序并發(fā)階段,(這個應用程序)被稱為 mutator (翻譯為“賦值器”),是它在垃圾回收進行時改變了變量的引用。
因為 mutator 必須保持沒有黑色對象指向白色對象,以免 GC 跟丟它在堆上已經(jīng)訪問過的變量。
維持這種不變是 寫屏障 (write barrier)的職責,它是 mutator 在堆中的指針被更新時運行的一個小函數(shù)(function)。
如果當前可達對象是白色,Go 的寫屏障標記它為灰色,以確保 GC 最終能掃描到它的引用。
確認在什么時候所有的灰色對象被完全標記完了很微妙,而且如果我們想避免 block 應用程序(mutators)代價非常昂貴而且復雜。為了讓事情簡單, Go 1.5 做足了工作讓它可以并行,然后短暫地 STW 去檢查所有潛在的灰色對象。找到最終 STW 所需的時間與該 GC 所做的工作總量之間的最佳結合點是在 Go 1.6 主要交付的內(nèi)容。
魔鬼在于細節(jié)。GC 過程的具體細節(jié)的低級討論就不在這里討論了(可參考原文有哪些問題)。
在更高級別上,解決 Go 性能問題的方法是增加了 GC 旋鈕 (knobs),每一個性能問題有一個旋鈕。程序員可以旋轉這個旋鈕來為他們的應用程序尋找合適的設置。不利的一面是,在十年之后你每年使用一個或兩個新的旋鈕,你終將遇到《GC 旋鈕開關使用法》(GC Knobs Turner Employment Act),Go 不會走這條路。轉而,Go 提供了一個旋鈕,它叫 GOGC。
GOGC 控制了涉及到可達對象空間的堆的總大小。默認是 100,意味著在上一次收集之后,堆的大小比可達對象的空間大小大 100% 倍(也就是兩倍大)。200 就意味著是 3 倍大。如果你想降低 GC 的總時間,那就增加 GOGC;如果你想用更多的 GC 時間來換取更少的內(nèi)存,那就降低 GOGC。
更重要得,隨著下一代硬件中 RAM 增倍(雙倍),簡單地設置 GOGC 增倍(雙倍)將使用 GC 循環(huán)數(shù)減半。另一方面,既然 GOGC 是根據(jù)可達對象的空間大小來的,將可達對象增倍來讓負載增倍不需要重調(diào)(retune)。應用自己擴展了。更進一步得,沒有眾多旋鈕(knob)的困擾,團隊可以專注于客戶應用的反饋提高運行性能。
Go 1.5 指向了一個未來,在未來 STW 的暫停不再是使用安全語言的屏障,應用可以輕松得與硬件擴展,并且隨著硬件變得越來越強大,GC 將不會成為更好、更具可擴展性的軟件的障礙。
更多 1.5GC 的相關內(nèi)容及我們怎么降低延遲問題可以參考: Go GC: Latency Problem Solved presentation 和 the slides 。
上面提到了這些術語和概念:
下面賦兩張 PPT 中的圖:
gc 與gccgo 都是go語言標準規(guī)范的不同實現(xiàn),兩者包含不同的側重點:
使用成本上gccgo遠比gc更高,基于如下原因:
總結:除非真要追求高性能,否則不建議去折騰gccgo
如果一定要折騰,建議思路:基于gcc docker 鏡像,編寫Dockerfile,安裝golang,然后使用 go build -compiler=gccgo 。
相關資源:
Go 語言較之 C 語言一個很大的優(yōu)勢就是自帶 GC 功能,可 GC 并不是沒有代價的。寫 C 語言的時候,在一個函數(shù)內(nèi)聲明的變量,在函數(shù)退出后會自動釋放掉,因為這些變量分配在棧上。如果你期望變量的數(shù)據(jù)可以在函數(shù)退出后仍然能被訪問,就需要調(diào)用 malloc 方法在堆上申請內(nèi)存,如果程序不再需要這塊內(nèi)存了,再調(diào)用 free 方法釋放掉。Go 語言不需要你主動調(diào)用 malloc 來分配堆空間,編譯器會自動分析,找出需要 malloc 的變量,使用堆內(nèi)存。編譯器的這個分析過程就叫做逃逸分析。
所以你在一個函數(shù)中通過 dict := make(map[string]int) 創(chuàng)建一個 map 變量,其背后的數(shù)據(jù)是放在??臻g上還是堆空間上,是不一定的。這要看編譯器分析的結果。
可逃逸分析并不是百分百準確的,它有缺陷。有的時候你會發(fā)現(xiàn)有些變量其實在??臻g上分配完全沒問題的,但編譯后程序還是把這些數(shù)據(jù)放在了堆上。如果你了解 Go 語言編譯器逃逸分析的機制,在寫代碼的時候就可以有意識地繞開這些缺陷,使你的程序更高效。
Go 語言雖然在內(nèi)存管理方面降低了編程門檻,即使你不了解堆棧也能正常開發(fā),但如果你要在性能上較真的話,還是要掌握這些基礎知識。
這里不對堆內(nèi)存和棧內(nèi)存的區(qū)別做太多闡述。簡單來說就是, 棧分配廉價,堆分配昂貴。 ??臻g會隨著一個函數(shù)的結束自動釋放,堆空間需要時間 GC 模塊不斷地跟蹤掃描回收。如果對這兩個概念有些迷糊,建議閱讀下面 2 個文章:
這里舉一個小例子,來對比下堆棧的差別:
stack 函數(shù)中的變量 i 在函數(shù)退出會自動釋放;而 heap 函數(shù)返回的是對變量 i 的引用,也就是說 heap() 退出后,表示變量 i 還要能被訪問,它會自動被分配到堆空間上。
他們編譯出來的代碼如下:
邏輯的復雜度不言而喻,從上面的匯編中可看到, heap() 函數(shù)調(diào)用了 runtime.newobject() 方法,它會調(diào)用 mallocgc 方法從 mcache 上申請內(nèi)存,申請的內(nèi)部邏輯前面文章已經(jīng)講述過。堆內(nèi)存分配不僅分配上邏輯比??臻g分配復雜,它最致命的是會帶來很大的管理成本,Go 語言要消耗很多的計算資源對其進行標記回收(也就是 GC 成本)。
Go 編輯器會自動幫我們找出需要進行動態(tài)分配的變量,它是在編譯時追蹤一個變量的生命周期,如果能確認一個數(shù)據(jù)只在函數(shù)空間內(nèi)訪問,不會被外部使用,則使用??臻g,否則就要使用堆空間。
我們在 go build 編譯代碼時,可使用 -gcflags '-m' 參數(shù)來查看逃逸分析日志。
以上面的兩個函數(shù)為例,編譯的日志輸出是:
日志中的 i escapes to heap 表示該變量數(shù)據(jù)逃逸到了堆上。
需要使用堆空間,所以逃逸,這沒什么可爭議的。但編譯器有時會將 不需要 使用堆空間的變量,也逃逸掉。這里是容易出現(xiàn)性能問題的大坑。網(wǎng)上有很多相關文章,列舉了一些導致逃逸情況,其實總結起來就一句話:
多級間接賦值容易導致逃逸 。
這里的多級間接指的是,對某個引用類對象中的引用類成員進行賦值。Go 語言中的引用類數(shù)據(jù)類型有 func , interface , slice , map , chan , *Type(指針) 。
記住公式 Data.Field = Value ,如果 Data , Field 都是引用類的數(shù)據(jù)類型,則會導致 Value 逃逸。這里的等號 = 不單單只賦值,也表示參數(shù)傳遞。
根據(jù)公式,我們假設一個變量 data 是以下幾種類型,相應的可以得出結論:
下面給出一些實際的例子:
如果變量值是一個函數(shù),函數(shù)的參數(shù)又是引用類型,則傳遞給它的參數(shù)都會逃逸。
上例中 te 的類型是 func(*int) ,屬于引用類型,參數(shù) *int 也是引用類型,則調(diào)用 te(j) 形成了為 te 的參數(shù)(成員) *int 賦值的現(xiàn)象,即 te.i = j 會導致逃逸。代碼中其他幾種調(diào)用都沒有形成 多級間接賦值 情況。
同理,如果函數(shù)的參數(shù)類型是 slice , map 或 interface{} 都會導致參數(shù)逃逸。
匿名函數(shù)的調(diào)用也是一樣的,它本質(zhì)上也是一個函數(shù)變量。有興趣的可以自己測試一下。
只要使用了 Interface 類型(不是 interafce{} ),那么賦值給它的變量一定會逃逸。因為 interfaceVariable.Method() 先是間接的定位到它的實際值,再調(diào)用實際值的同名方法,執(zhí)行時實際值作為參數(shù)傳遞給方法。相當于 interfaceVariable.Method.this = realValue
向 channel 中發(fā)送數(shù)據(jù),本質(zhì)上就是為 channel 內(nèi)部的成員賦值,就像給一個 slice 中的某一項賦值一樣。所以 chan *Type , chan map[Type]Type , chan []Type , chan interface{} 類型都會導致發(fā)送到 channel 中的數(shù)據(jù)逃逸。
這本來也是情理之中的,發(fā)送給 channel 的數(shù)據(jù)是要與其他函數(shù)分享的,為了保證發(fā)送過去的指針依然可用,只能使用堆分配。
可變參數(shù)如 func(arg ...string) 實際與 func(arg []string) 是一樣的,會增加一層訪問路徑。這也是 fmt.Sprintf 總是會使參數(shù)逃逸的原因。
例子非常多,這里不能一一列舉,我們只需要記住分析方法就好,即,2 級或更多級的訪問賦值會 容易 導致數(shù)據(jù)逃逸。這里加上 容易 二字是因為隨著語言的發(fā)展,相信這些問題會被慢慢解決,但現(xiàn)階段,這個可以作為我們分析逃逸現(xiàn)象的依據(jù)。
下面代碼中包含 2 種很常規(guī)的寫法,但他們卻有著很大的性能差距,建議自己想下為什么。
Benchmark 和 pprof 給出的結果:
熟悉堆棧概念可以讓我們更容易看透 Go 程序的性能問題,并進行優(yōu)化。
多級間接賦值會導致 Go 編譯器出現(xiàn)不必要的逃逸,在一些情況下可能我們只需要修改一下數(shù)據(jù)結構就會使性能有大幅提升。這也是很多人不推薦在 Go 中使用指針的原因,因為它會增加一級訪問路徑,而 map , slice , interface{} 等類型是不可避免要用到的,為了減少不必要的逃逸,只能拿指針開刀了。
大多數(shù)情況下,性能優(yōu)化都會為程序帶來一定的復雜度。建議實際項目中還是怎么方便怎么寫,功能完成后通過性能分析找到瓶頸所在,再對局部進行優(yōu)化。
網(wǎng)站標題:go1.9語言gc go語言標準庫中文版
當前URL:http://jinyejixie.com/article16/hpcegg.html
成都網(wǎng)站建設公司_創(chuàng)新互聯(lián),為您提供品牌網(wǎng)站制作、軟件開發(fā)、品牌網(wǎng)站建設、面包屑導航、用戶體驗、企業(yè)建站
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉載內(nèi)容為主,如果涉及侵權請盡快告知,我們將會在第一時間刪除。文章觀點不代表本網(wǎng)站立場,如需處理請聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉載,或轉載時需注明來源: 創(chuàng)新互聯(lián)