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