[翻译]Go与C#的比较,第二篇:垃圾回收
Go vs C#, part 2: Garbage Collection | by Alex Yakunin | ServiceTitan?—?Titan Tech | Medium
目錄
譯者注
什么是垃圾回收?
什么是GCBurn?
峰值分配吞吐量("速度測試")
GCBurn 測試
GC Burn測試結(jié)果
結(jié)論
.NET Core
Go
兩者的相同點
免責聲明和后記
譯者注
本文90%通過機器翻譯,另外10%譯者按照自己的理解進行翻譯,和原文相比有所刪減,可能與原文并不是一一對應(yīng),但是意思基本一致。
這是Alex Yakunin大佬關(guān)于Go和C#比較的第二篇文章,本文發(fā)表于2018年9月,當時使用的.NET Core版本應(yīng)該是2.1,Go版本應(yīng)該是1.11版本。而現(xiàn)在.NET版本已經(jīng)到6 Pre5,Go也到了1.16,經(jīng)過這么多版本的迭代,Go和.NET的GC性能都有很大提高,所以數(shù)據(jù)僅供參考,當然也歡迎大家能在新的版本上跑一下最新的結(jié)果發(fā)一篇帖子出來。
譯者水平有限,如果錯漏歡迎批評指正
譯者@Bing Translator、@InCerry,另外感謝@曉青、@賈佬、@曉晨、@黑洞、@maaserwen、@帥張、@3wlinecode、@huchenhao百忙之中抽出時間幫忙review和檢查錯誤。
原文鏈接:https://medium.com/servicetitan-engineering/go-vs-c-part-2-garbage-collection-9384677f86f1
這一個系列中還有其他兩篇文章:
第一篇:Goroutines vs Async-Await?【中文翻譯版】
第三篇:Compiler, Runtime, Type System, Modules, and Everything Else. 【中文翻譯版】
有趣的是,這篇文章的草稿是幾個月前寫的,而且比較短。它的主要內(nèi)容是。"Go的GC顯然比.NET的差,請看下面的帖子。1,?2,?3,?4(注意,其中有些是最近的),以了解詳情"。
但是......我還是想讓自己以某種方式測試這個問題,所以我請我的一個朋友 - Go專家?guī)臀易鲞@個基準測試。我們寫了GCBurn,一個相對簡單的垃圾收集和內(nèi)存分配基準,目前支持Go和C#,盡管你可以自由地把它移植到任何其他有GC的語言上。
現(xiàn)在,讓我們進入森林吧 ???? 【應(yīng)該是俚語,來自電影https://en.wikipedia.org/wiki/Into_the_Woods_(film)】
什么是垃圾回收?
這個帖子相當長,所以如果你知道GC是什么,請?zhí)^這一部分。
垃圾回收(GC,Garbage Collector)是運行時的一部分,負責回收 "死 "對象使用的內(nèi)存,下面是它的工作原理:
"活著的 "對象是堆中的任何對象,它要么現(xiàn)在被使用(它的一個指針被存儲在CPU的一個寄存器中),要么將來可能被使用(可能有一個程序最終獲得了這樣一個對象的指針)。如果你把堆看成是一個對象相互引用的圖,很容易注意到,如果某個對象O是活的,那么它直接引用的每個對象(O1, O2, ... O_m)也是活的:有一個指向O的指針,你可以通過一條CPU指令獲得指向O1, O2, ... O_m的指針。對于O1, O2, ...O_m所引用的對象也可以這樣說--這些對象中的每一個都是活的。換句話說,如果對象Q可以從某個活著的對象A處到達,Q也是活著的【可達性分析】。
"死 "對象是堆中除了"活著的"所有其他對象。它們是 "死 "的,因為代碼沒有辦法在將來以某種方式獲得它們中任何一個對象的指針。沒有辦法找到它們,因此也沒有辦法使用它們。
一個很好的現(xiàn)實世界的比喻是:假設(shè)你從有機場(GC根)的任何城市開始旅行,你想找出哪些城市(對象)是可以通過公路網(wǎng)到達的。
圍繞單一原點的可達區(qū)域的可視化。圖片來源?https://www.graphhopper.com/blog/2018/07/04/high-precision-reachability/
這個定義也解釋了垃圾收集算法的基本部分:它必須不時地檢查什么是可觸及的(活著的),并刪除其他一切。下面是通常的步驟:
凍結(jié)所有線程。
將所有GC根(從CPU寄存器、定位器/調(diào)用堆棧框架或靜態(tài)字段引用的對象,即所有正在使用或立即可用的東西)標記為活的。
把每一個可以從GC根部接觸到的物體也標記為活的,其他的都視為死的。
讓死亡對象分配的內(nèi)存再次可用,例如,你可以把它標記為 "可供未來分配",或者通過移動所有活著的對象來整理堆,使之沒有空隙。
最后,解凍所有線程。
這里所描述的通常被稱為 "標記和清掃?"GC,它是最直接的實現(xiàn),但不是最有效的。它意味著我們必須暫停一切來執(zhí)行GC,這就是為什么有這種暫停的收集器也被稱為Stop-the-World,或者STW收集器--與無暫停收集器相反pauseless collectors.。
在解決問題的方式上,無暫停與STW收集器沒有什么不同,它們將與你的代碼同時進行幾乎所有的工作。顯然,這是很棘手的,如果我們回到現(xiàn)實世界中的城市和道路的比喻,這就像試圖繪制從機場可以到達的城市的地圖,假設(shè):
你實際上沒有地圖,但有一個由你操作的車隊。
當這些車輛行駛時,新的城市和道路被建造,一些道路被摧毀。
所有這些都使問題變得更加復雜,特別是在這種情況下,你不能像以前那樣修建道路:你必須檢查艦隊目前是否正在運行(即GC正在尋找活著的對象),以及它是否已經(jīng)通過了你新修建的道路的起點城市(即GC已經(jīng)將該城市標記為活著)。如果是這樣,你必須通知車隊(GC)回到那里,找到所有可以通過新路到達的城市。
翻譯成我們的非虛構(gòu)案例,它意味著當GC運行時,任何指針寫操作都需要一個特殊的檢查了(寫屏障),而且它會拖慢你的代碼。
無暫停和STW GC之間沒有黑白之分,這只是關(guān)于STW停頓的時間。
STW的暫停時間如何取決于不同的因素?例如,它是固定的,還是與活著的對象集的大小成比例(O(alive_set_size)【時間復雜度】)?
如果這些停頓是固定的,那么實際的持續(xù)時間是多少?如果它對你的特定情況來說是很小的,那么它就~與完全無暫停的 GC 相同。
如果這些暫停不是固定的,我們能否確保它們永遠不會超過我們能承受的最大限度?
最后,請注意,不同的GC實現(xiàn)可能針對不同的方面進行優(yōu)化:
由GC引起的整體減速(或整體程序吞吐量):即大致上,花在GC上的時間百分比+所有相關(guān)性能損耗(例如上面例子中的寫障礙檢查)。
STW暫停時間的分布:顯然,越短越好(抱歉,這里沒有雙關(guān)語)+理想情況下,你不希望有O(aliveSetSize)停頓。
總的來說,花在GC上的內(nèi)存的百分比,或由于其具體的實現(xiàn)。額外的內(nèi)存可能被分配器或GC直接使用,也可能因為堆碎片化而不可用,等等。
內(nèi)存分配的吞吐量:GC通常與內(nèi)存分配器緊密結(jié)合。特別是,分配器可能會觸發(fā)當前線程暫停來完成GC的一部分工作,或者使用更昂貴的數(shù)據(jù)結(jié)構(gòu)來實現(xiàn)某種GC。
等等。-?這里還列舉了很多因素。
最糟糕的是:你顯然不能得到所有的好處,也就是說,不同的GC實現(xiàn)有它們自己的好處和取舍。這就是為什么很難寫出一個好的GC基準:)
什么是GCBurn?
GCBurn是我們精心設(shè)計的一個基準,用于直接測量最重要的GC性能指標--即通過實際來直接測量【作者的意思應(yīng)該是直接通過觀察OS報告的進程狀態(tài),來觀測】,而不是查詢運行時提供的性能計數(shù)器或API。
直接測量提供了一些好處:
移植性:將我們的基準移植到大多數(shù)的運行時中是相當容易的,它是否具有允許以某種方式查詢我們所測量的內(nèi)容的API根本就不重要。而且我們絕對歡迎你這樣做。例如,我真的很想看看Java與Go和.NET Core的對比情況。
更少的有效性問題:也更容易驗證你得到的數(shù)據(jù)是否真的有效:從運行時得到同樣的數(shù)字總是會引起一些問題,比如 "你怎么能確定你得到的是正確的數(shù)字?",而這些問題的良好答案意味著你可能會花更多的時間研究特定的GC實現(xiàn)和收集指標的方式,而不是編寫一個類似的測試。
其次,GCBurn的設(shè)計是為了將蘋果與蘋果進行比較,也就是說,它的所有測試都做了幾乎完全相同的動作/分配序列--依賴相同的分布、相同的隨機數(shù)生成器,等等。這就是為什么它對不同語言和框架的測試結(jié)果可以直接進行比較。
GCBurn進行了兩項測試:
峰值分配吞吐量("速度測試")
這里的意圖是測量峰值突發(fā)分配率,假設(shè)沒有其他東西(特別是GC)會減慢內(nèi)存分配的速度:
啟動T個線程/goroutines,其中每個線程:
盡可能快地分配16字節(jié)的對象(有兩個int64字段的對象)。
循環(huán)進行,持續(xù)時間為1ms,跟蹤分配的總次數(shù)。
等待所有的線程完成,以及分配率(每秒的對象)。
重復這個過程~30次,并打印最大的測量分配率。
GCBurn 測試
這是一個更復雜的測試:
持續(xù)分配的峰值吞吐量(對象/秒,字節(jié)/秒)--即在一個相對較長的時間段內(nèi)測得的吞吐量,假設(shè)我們分配、保持并最終釋放每個對象,并且這些對象的大小和保持時間遵循接近于現(xiàn)實生活的分布。
由GC引起的線程暫停的頻率和持續(xù)時間分布,50%百分位數(shù)(p50)、p95、p99、p99.9、p99.99+最小、最大和平均值。
由GC引起的STW(全局)停頓的頻率和時間分布。
下面是測試的工作方式:
分配適當大小的 "靜態(tài)集"(我會進一步解釋)。
啟動T線程/goroutines,其中每個線程:
按照預先生成的大小和壽命分布模式分配對象(實際上是int64s的數(shù)組/片)。該模式實際上是一個由3個值組成的圖元列表:(size, ~floor(log10(duration)), str(duration)[0] - '0')。最后兩個值編碼 "保持時間",它的指數(shù)和它的十進制表示法中的第一個數(shù)字,單位是微秒。這是一項優(yōu)化,允許 "釋放 "操作相當有效,每個分配的對象有O(1)的時間復雜度,我們在這里用一點精度來換取速度。
每16次分配,嘗試釋放那些保持時間已經(jīng)過期的分配對象。
對于每個循環(huán)迭代,測量當前迭代所花費的時間。如果花費的時間超過10微秒(通常情況下,迭代的時間應(yīng)該小于0.1微秒),假設(shè)有一個GC暫停,所以將其開始和結(jié)束時間記錄在這個線程的列表中。
追蹤分配的數(shù)量和分配對象的總大小。D秒后停止。
等待所有的線程都完成。
當上述部分完成后,每個線程的情況如下:
它能夠執(zhí)行的分配的數(shù)量,以及它們的總大小(字節(jié))。
它所經(jīng)歷的停頓(停頓間隔)列表。
有了每個線程的這些列表,就有可能計算出STW暫停的時間間隔列表(即每個線程暫停的時間段),只需將所有這些列表相交即可。
了解了這些,就很容易產(chǎn)生上述的統(tǒng)計數(shù)據(jù)。
現(xiàn)在,一些重要的細節(jié):
我已經(jīng)提到,分配序列(模式)是預先生成的。這樣做主要是因為我們不想為每一次分配花費CPU周期來生成一組隨機數(shù)。生成的序列是由~ 1M個~(大小,log(持續(xù)時間))的項目組成。參見BurnTester.TryInitialize(C#?/?Go)以查看實際實現(xiàn)。
每個GCBurn線程使用相同的序列,但從那里的一個隨機點開始。當它到達終點時,它從序列的開頭繼續(xù)。
為了確保每種語言的模式絕對相同,我們使用了自定義的隨機數(shù)發(fā)生器(見StdRandom.cs?/?std_random.go)。實際上,它是C++ 11的minstd_rand實現(xiàn),移植到C#和Go。
而且總的來說,我們確保所有我們使用的隨機值在不同的平臺上都是相同的,分配序列中的線程起點,這個序列中的大小和持續(xù)時間,等等。
大小
我們使用的對象大小和保持時間分布(見樣例,?C#,?Go)是為了接近現(xiàn)實的實際情況。
99%的 "典型 "對象 + 0.99%的 "大型 "對象+0.01%的 "超大型",其中:
"典型"大小遵循正態(tài)分布,平均值=32字節(jié),stdDev=64字節(jié)
"大"尺寸遵循對數(shù)正態(tài)分布,基礎(chǔ)正態(tài)分布的平均值=log(2 Kb)=11,stdDev=1。
"超大"尺寸遵循對數(shù)正態(tài)分布,基礎(chǔ)正態(tài)分布的平均值=log(64 Kb)=16,stdDev=1。
尺寸被截斷以適應(yīng)[32B ... 128KB]的范圍,然后轉(zhuǎn)化為數(shù)組/片斷尺寸,考慮到C#的參考尺寸(8B)和數(shù)組頭尺寸(24B),以及Go的片斷尺寸(24B)。
對象保持時間
同樣,它由95%的 "方法級 " + 4.9%的 "請求級 " + 0.1%的 "長效 "保持時間組成,其中:
方法級"保持時間遵循正態(tài)分布變量的絕對值,平均值=0微秒,stdDev=0.1微秒
"請求級"保持時間遵循類似的分布,但stdDev=100ms(毫秒)。
"長壽"保持時間遵循正態(tài)分布,平均值=stdDev=10秒。
保持時間被截斷以適應(yīng)[0 ... 1000秒]范圍。
最后,靜態(tài)集是一組遵循完全相同的大小分布的對象,在測試過程中從未釋放過,換句話說,它是我們的活體集。如果你的應(yīng)用程序在RAM中緩存或存儲了大量的數(shù)據(jù)(或有一些內(nèi)存泄漏),它將會很大。同樣,對于簡單的無狀態(tài)應(yīng)用程序(如簡單的網(wǎng)絡(luò)/API服務(wù)器),它應(yīng)該是小的。
如果你讀到這里,你可能急于看到結(jié)果,結(jié)果在這里。
GC Burn測試結(jié)果
我們已經(jīng)在一組非常不同的機器上運行了test-all(或Windows上的Test-All.bat),并將輸出轉(zhuǎn)存到了結(jié)果文件夾.。
Test-all運行以下測試:
峰值分配吞吐量測試("速度測試"):使用1、25%、50%、75%和100%的最大。# 系統(tǒng)實際可以并行運行的線程數(shù)。因此,例如,對于Core i7-8700K,"100%線程"=12個線程(6個核心*每個核心2個線程/超線程)。
GCBurn測試:對于靜態(tài)設(shè)置大小=0MB、1MB、10%、25%、50%和75%的測試機器上的總內(nèi)存,并使用100%的最大線程。# 線程數(shù)。每個測試運行2分鐘。
GCBurn測試:所有的設(shè)置與前面的情況相同,但使用75%的最大。# 系統(tǒng)實際可以并行運行的線程數(shù)的75%。
最后,它以3種模式對.NET運行所有這些測試--服務(wù)器GC+SustainedLowLatency(你可能會在你的生產(chǎn)服務(wù)器上使用這種模式),服務(wù)器GC+Batch,以及工作站GC。我們對Go也做了同樣的測試--唯一相關(guān)的選項是GOGC,但我們在將其設(shè)置為50%后沒有注意到任何區(qū)別:似乎Go在這個測試中連續(xù)運行GC~。
所以我們開始吧。你也可以打開Google電子表格,里面有我用于制作圖表的所有數(shù)據(jù),以及GitHub上的 "結(jié)果"文件夾,里面有原始測試輸出(那里有更多的數(shù)據(jù))。
下圖峰值吞吐量(越大越好),單位M ops/秒,測試平臺12核非虛擬化的英特爾酷睿i7-8700K CPU @ 3.70GHz
下圖峰值吞吐量(越大越好),M ops/秒,測試平臺96核AWS m5.24xlarge實例(硬件CPU:Intel Xeon Platinum 8175M CPU @ 2.50GHz)
提醒一下,這個測試的1個操作=分配一個16字節(jié)的對象。
在突發(fā)分配方面,.NET顯然勝過Go:
它不僅在單線程測試中快了3倍(Ubuntu)......5倍(Windows),而且隨著線程數(shù)量的增加,它的擴展性也更好,在96核的怪物m5.24xlarge上,差距擴大到12倍。
堆分配在.NET上是幾乎不損耗性能的操作。如果你看一下數(shù)字,它們實際上只是比棧分配多耗3-4倍性能:你在每個線程上每秒進行約10億的簡單調(diào)用,與3億的堆分配成本差不多。
看起來.NET在Windows上更快一些,相反,Go在Windows與Linux上相比幾乎慢了2倍。
下圖持續(xù)吞吐量(越大越好),M ops/秒,12線程,測試平臺12核非虛擬化的英特爾酷睿i7-8700K CPU @ 3.70GHz
下圖持續(xù)吞吐量(越大越好),M ops/秒,16線程,測試平臺96核AWS m5.24xlarge實例(硬件CPU:Intel Xeon Platinum 8175M CPU @ 2.50GHz)
在這個測試上的一個操作是按照前面描述的模擬現(xiàn)實生活分布進行的一次分配;此外,在這個測試上分配的每個對象都有一個按照另一個模擬現(xiàn)實場景分布的壽命。
在這個測試中,.NET仍然更快,盡管差距并不大,20 ... 50%取決于操作系統(tǒng)(Linux上更小,Windows上更大)和靜態(tài)集大小。
你可能還注意到,Go不能通過 "靜態(tài)集合大小=50%內(nèi)存/75%內(nèi)存 "的測試,它以O(shè)OM(內(nèi)存不足)失敗。在75%的可用CPU核心上運行測試有助于Go通過 "靜態(tài)集=50% RAM "測試,但在75%的情況下仍然無法通過。
下圖持續(xù)吞吐量(越大越好),M ops/秒,9-12線程,測試平臺12核非虛擬化的英特爾酷睿i7-8700K CPU @ 3.70GHz
下圖持續(xù)吞吐量(越大越好),M ops/秒,12-16線程,測試平臺96核AWS m5.24xlarge實例(硬件CPU:Intel Xeon Platinum 8175M CPU @ 2.50GHz)
延長測試時間(這里是2分鐘的測試時間)會導致Go在 "靜態(tài)集=50%內(nèi)存 "的測試中幾乎每次都發(fā)生OOM,從結(jié)果上來看,如果活著的靜態(tài)集足夠大,那么GO的GC無法跟上分配速度。
除此之外,使用100%和75%的CPU核心測試吞吐量率之間沒有任何顯著變化。
同樣明顯的是,Go和.NET的分配器都沒有隨著核心數(shù)量的增加而得到吞吐量的增加。這個圖表證明了這一點。
下圖持續(xù)吞吐量(越大越好),M ops/秒,72-96線程,測試平臺96核AWS m5.24xlarge實例(硬件CPU:Intel Xeon Platinum 8175M CPU @ 2.50GHz)
正如你所看到的,多出5倍的CPU核心數(shù)量在這里只轉(zhuǎn)化為2.5倍的速度提升。
看起來內(nèi)存帶寬并不是瓶頸:~70 M ops/sec.轉(zhuǎn)換為~6.5 GB/sec.,這只是Core i7機器上可用內(nèi)存帶寬的10%。
同樣有趣的是,Go在 "靜態(tài)集=50%內(nèi)存 "的情況下開始擊敗.NET。你想知道為什么嗎?
下圖最大STW停頓時間(越小越好),ms,9-12線程,測試平臺12核非虛擬化的英特爾酷睿i7-8700K CPU @ 3.70GHz
下圖最大STW停頓時間(越小越好),ms,72-96線程,測試平臺96核AWS m5.24xlarge實例(硬件CPU:Intel Xeon Platinum 8175M CPU @ 2.50GHz)
是的,這對.NET來說是一個絕對可恥的部分:
Go的暫停在這里幾乎看不出來,因為暫停時間很小。我能夠測量到的最大暫停時間是1.3秒,作為比較,.NET在同一測試案例中得到125秒的STW暫停。
Go中幾乎所有的STW停頓都是亞毫秒級的。如果你看一下更多真實的測試案例(例如這個文件),你會發(fā)現(xiàn)在一個~普通的16核服務(wù)器上的16GB靜態(tài)設(shè)置意味著你最長的停頓=50ms(與.NET的5s相比),99.99%的停頓都短于7ms(.NET的92ms)!
對于.NET和Go來說,STW的暫停時間似乎與靜態(tài)集的大小成線性關(guān)系。但如果我們比較較大的暫停,Go的暫停時間要短100倍。
總結(jié)。Go的增量GC確實有效;而.NET的并發(fā)GC則不然。
好吧,可能我在這一點上把所有的.NET開發(fā)者都嚇壞了,特別是假設(shè)我已經(jīng)提到GCBurn的設(shè)計是接近真實生活的。那么你是否有望在.NET上得到類似的停頓?是的,也不是。
185GB(這是約20億個對象,GC暫停時間實際上取決于這個數(shù)字,而不是工作集的GB大小)的靜態(tài)集遠遠超出了你在現(xiàn)實生活中的預期。可能,即使是16GB的靜態(tài)集也遠遠超出了你在任何設(shè)計良好的應(yīng)用程序中可能看到的情況。
"設(shè)計良好"實際上意味著。"根據(jù)這里的發(fā)現(xiàn),沒有哪個正常的開發(fā)者會使用這么多GB的靜態(tài)集來制作一個.NET應(yīng)用程序"。有很多方法可以克服這個限制,但最終,所有這些方法都迫使你把數(shù)據(jù)存儲在巨大的托管數(shù)組中,或者存儲在非托管緩沖區(qū)中。.NET Core 2.1?--更確切地說,它的ref結(jié)構(gòu)、Span<T>和Memory<T>大大簡化了這些工作。
除此之外,"精心設(shè)計"還意味著"沒有內(nèi)存泄漏"。正如你可能注意到的,如果你在.NET中泄漏引用,你會看到有越來越長的STW暫停。很明顯,你的應(yīng)用程序最終會崩潰,但請注意,在崩潰發(fā)生之前,它也可能變得暫時沒有反應(yīng)--所有這些都是因為STW暫停時間越來越長。而你額外內(nèi)存泄漏的越多,情況就會越糟糕。
追蹤最大。GC暫停時間和Gen2后的GC工作集大小對于確保你不會因為內(nèi)存泄漏而遭受p95-p99延時越來越大來說,一定是至關(guān)重要的。
作為.NET開發(fā)者,我真的希望.NET核心團隊能早點解決max_STW_pause_time = O(static_set_size)的問題。除非它得到解決,否則.NET開發(fā)者將不得不依賴變通方法,這實際上一點也不好。最后,即使它的存在也會對許多潛在的.NET應(yīng)用起到阻礙作用--想想物聯(lián)網(wǎng)、機器人和其他控制應(yīng)用;高頻交易、游戲或游戲服務(wù)器等。
至于Go,這個問題在那里得到了很好的解決,令人驚訝。值得注意的是,Go團隊從2014年開始就一直在與STW暫停作斗爭--最終,他們成功地殺死了所有O(alive_set_size)暫停(正如該團隊所聲稱的--似乎測試并不能證明這一點,但也許這只是因為GCBurn走得太遠而暴露了這一點 ???? ). 無論如何,如果你對那里發(fā)生的細節(jié)感興趣,這個帖子是一個很好的開始:https://blog.golang.org/ismmkeynote
我在問自己,在這兩個選項中,我更喜歡哪一個,即.NET更快的分配器和Go的微小GC暫停。坦率地說,我更傾向于Go--主要是因為它的分配器的性能看起來還不錯,但在大堆上100倍的短暫停頓是相當有吸引力的。至于大堆上的OOM(或需要2倍以上的內(nèi)存來避免OOM)--嗯,內(nèi)存很便宜。雖然如果你在同一臺機器上運行多個Go應(yīng)用,這可能更重要(想想桌面應(yīng)用和微服務(wù))。
總而言之,STW停頓的這種情況讓我羨慕Go開發(fā)者所擁有的東西--可能,這是第一次。
好了,還有最后一個話題要講--即.NET上的其他GC模式(劇透:它們不能拯救世界,但仍然值得一談)【.NET GC有很多的模式,想知道詳情可以點我】。
下圖突發(fā)吞吐量(越大越好),M ops/秒
并發(fā)模式下的服務(wù)器GC(SustainedLowLatency或Interactive)提供了最高的峰值吞吐量,盡管與Batch的差別很小。
下圖持續(xù)吞吐量(越大越好),M ops/秒
在服務(wù)器GC+SLL模式下,持續(xù)的吞吐量也是最高的。服務(wù)器 GC + 批量模式也非常接近,但工作站 GC 根本無法隨著靜態(tài)集規(guī)模的增長而擴展。
最后,STW時間:
下圖最大STW停頓時間(越小越好),ms
我不得不添加這個表格(來自提到的谷歌電子表格--見那里的最后一張表格)來展示具體的數(shù)據(jù):
工作站GC實際上只有在靜態(tài)集大小< 16 GB時才有較小的STW停頓;超過16 GB后,從這個角度來看,工作站GC的STW越來越大--與服務(wù)器GC + Batch模式相比,在48 GB的情況下,工作站GC的STW時間幾乎增加了3倍。
有趣的是,在靜態(tài)集大小≥16GB時,服務(wù)器GC+Batch開始擊敗服務(wù)器GC+SLL--也就是說,在大堆上,批處理模式的GC實際上比并發(fā)GC的暫停時間要小。
最后,服務(wù)器GC + SLL和服務(wù)器GC + Batch在暫停時間方面實際上是相當相似的。也就是說,.NET上的并發(fā)GC顯然沒有做太多的并發(fā)工作--盡管它在我們的具體案例中實際上可能是相當高效的。我們在主測試之前創(chuàng)建了靜態(tài)集,所以似乎沒有必要重新定位--幾乎所有GC要做的工作就是標記活著的對象,而這正是并發(fā)GC應(yīng)該做的事。因此,為什么它產(chǎn)生了與批處理GC幾乎一樣的可恥的長停頓,這完全是個謎。
你可能會問,為什么我們沒有測試服務(wù)器GC+Interactive mode--事實上,我們做了,但沒有注意到與服務(wù)器GC+SLL的明顯區(qū)別。
結(jié)論
.NET Core
在Gen2集合上有O(alive_object_count) STW暫停時間--無論你選擇什么GC模式。很明顯,這些暫停時間可以是任意長的--完全取決于你的活體集的大小。我們在200GB的堆上測量了125秒的暫停時間。
在分配突發(fā)事件上快得多(3 ... 12倍)--這種分配真的類似于.NET上的堆棧分配。
在持續(xù)的吞吐量測試中,通常會快20 ... 50%。"靜態(tài)集大小=200GB "是唯一的情況,當Go繼續(xù)前進。
你永遠不應(yīng)該在.NET Core服務(wù)器上使用工作站GC--或者至少你應(yīng)該準確地知道你的工作集小到足以讓它受益。
并發(fā)模式(SustainedLowLatency或Interactive)下的服務(wù)器GC似乎是一個很好的默認值--盡管它與Batch模式?jīng)]有什么區(qū)別,這實際上是很令人驚訝的。
Go
沒有O(alive_object_count)的STW暫停--更準確地說,似乎它實際上有O(alive_object_count)的暫停,但它們?nèi)匀槐?NET的短100倍。
幾乎所有的暫停都短于1ms;我們看到的最長的暫停是1.3秒--在一個巨大的~200GB的活體集上。
在GCBurn測試中,它比.NET慢。Windows + i7-8700K是我們測量到最大差異的地方--也就是說,似乎Go在Windows上的內(nèi)存分配器有一些問題。
Go無法處理 "靜態(tài)集=75%內(nèi)存 "的情況。Go上的這個測試總是觸發(fā)OOM。同樣,如果你運行這個測試足夠長的時間(2分鐘=~50%的失敗幾率,10分鐘--我記得只有一個案例沒有崩潰),它在"靜態(tài)集合=50%內(nèi)存 "的情況下也會可靠地失敗。似乎,GC根本無法跟上那里的分配速度,而像"只使用75%的CPU核心進行分配"這樣的事情也沒有幫助。不過不知道這在現(xiàn)實生活中是否可能很重要:分配是GCBurn的全部工作,而大多數(shù)應(yīng)用程序并不只是做這個。另一方面,持續(xù)的并發(fā)分配吞吐量通常低于非并發(fā)的峰值吞吐量,所以現(xiàn)實生活中的應(yīng)用程序在多核機器上產(chǎn)生類似的分配負荷看起來并不虛構(gòu)。
但是,即使考慮這些,在GC無法跟上分配速度的情況下,做什么更好也是可以爭論的:是暫停應(yīng)用幾分鐘,還是以O(shè)OM方式失敗。我打賭大多數(shù)開發(fā)者實際上更喜歡第二種選擇。
兩者的相同點
峰值分配速度與突發(fā)分配測試中的核心數(shù)呈線性關(guān)系。
另一方面,持續(xù)并發(fā)分配的吞吐量通常低于非并發(fā)的峰值吞吐量--也就是說,持續(xù)并發(fā)的吞吐量不能很好地擴展,無論是對于Go還是對于.NET。似乎不是因為內(nèi)存帶寬的問題:總的分配速度可以比可用帶寬低10倍。
免責聲明和后記
GCBurn被設(shè)計用來測量一些非常具體的指標。我們試圖讓它在某些方面接近現(xiàn)實生活,但顯然,這并不意味著它所輸出的數(shù)據(jù)就是你在實際應(yīng)用中一樣的。就像任何性能測試一樣,它的設(shè)計是為了測量它應(yīng)該測量的極值--而忽略了其他幾乎所有的東西。因此,請不要對它抱有更大的期望 ????
我知道方法論是可以爭論的--坦率地說,在這里很難找到不可以爭論的東西。因此,撇開小問題不談,如果你對為什么像我們這樣評價GC可能是大錯特錯,請留下你的意見。我一定會很樂意討論這個問題。
我相信有一些方法可以改進測試,而不需要大幅增加工作量或代碼。如果你知道如何做到這一點,請你也留下評論,或者干脆作出貢獻。
同樣,如果你在那里發(fā)現(xiàn)了一些bug,請你也這樣做。
我有意不關(guān)注GC的實現(xiàn)細節(jié)(代數(shù)、壓縮等)。這些細節(jié)顯然很重要,但有很多關(guān)于這方面的帖子,以及關(guān)于現(xiàn)代垃圾收集的一般帖子。不幸的是,幾乎沒有關(guān)于實際GC和分配性能的帖子。這就是我想做的事情。
如果你愿意把這個測試翻譯成其他語言(如Java),并寫一個類似的帖子,那將是非常了不起的。
至于我的 "Go vs C#"系列,下一篇文章將討論運行時和類型系統(tǒng)。由于我不認為有必要為此寫幾千個LOC測試,所以應(yīng)該不會花那么多時間--敬請期待吧
P.S. 查看我們的新項目。Stl.Fusion是一個適用于.NET Core和Blazor的開源庫,力爭成為您的實時應(yīng)用程序的第一選擇。它的統(tǒng)一狀態(tài)更新管道確實很獨特,讓人心動。
另外插播一個小廣告
[蘇州-同程旅行] - .NET后端研發(fā)工程師
招聘中級及以上工程師,優(yōu)秀應(yīng)屆生也可以,我會全程跟進,從職位匹配,到面試建議與準備,再到面試流程和每輪面試的結(jié)果等。大家可以直接發(fā)簡歷給我。
工作職責
負責全球前三中文在線旅游平臺機票業(yè)務(wù)系統(tǒng)的研發(fā)工作,根據(jù)需求進行技術(shù)文檔編寫和編碼工作
任職要求
擁有至少1年以上的工作經(jīng)驗,優(yōu)秀的候選人可放寬
熟悉.NET Core和ASP.Net Core
C#基礎(chǔ)扎實,了解CLR原理,包括多線程、GC等
有DDD 微服務(wù)拆分 重構(gòu)經(jīng)驗者優(yōu)先
能對線上常見的性能問題進行診斷和處理
熟悉Mysql Redis MongoDB等數(shù)據(jù)庫中間件,并且進行調(diào)優(yōu)
必須有扎實的計算機基礎(chǔ)知識,熟悉常用的數(shù)據(jù)結(jié)構(gòu)與算法,并能在日常研發(fā)中靈活使用
熟悉分布式系統(tǒng)的設(shè)計和開發(fā),包括但不限于緩存、消息隊列、RPC及一致性保證等技術(shù)
海量HC 歡迎投遞~
薪資福利
月薪:15K~30K 根據(jù)職級不同有所不同
年假:10天帶薪年假 春節(jié)提前1天放假 病假有補貼
年終:根據(jù)職級不同有 2-4 個月
餐補:有餐補,自有食堂
交通:有打車報銷
五險一金:基礎(chǔ)五險一金,12%的公積金、補充醫(yī)療、租房補貼等
節(jié)日福利:端午、中秋、春節(jié)有節(jié)日禮盒
通訊補貼:根據(jù)職級不同,每個月有話費補貼 50~400
簡歷投遞方式
大家把簡歷發(fā)到我郵箱即可,記得一定要附上聯(lián)系(微信 or 手機號)方式喲~
郵箱(這是啥格式大家都懂):aW5jZXJyeUBmb3htYWlsLmNvbQ==
總結(jié)
以上是生活随笔為你收集整理的[翻译]Go与C#的比较,第二篇:垃圾回收的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: NET问答: 为什么仅有 getter
- 下一篇: 日志ILog(文件日志/控制台日志/控件