設計模式之爭:新分配內存還是內存池?(含評測)

在上文中,我們使用C++和Java分別開發了一個隊列,可以作為時鐘發生器。今天我們將其用作度量工具。

今天的問題是:為每個新消息分配新內存,還是使用內存池?我觀察到的網上討論中,老派C程序員通常避免分配內存,而Java程序員則傾向於分配新內存。本文中我們將詳細分析兩種做法。

該問題適用於批處理或者軟實時應用。對批處理程序來說,程序的吞吐量更加重要,對於軟實時程序來說,則存在延遲問題,如果處理某個消息的時間太長,程序會錯過一些傳入的消息。本文將分別研究這兩種情況。事實上也存在第三種情況,網絡服務器,同時有延遲和吞吐量的限制,在本文中暫不討論。

關於實時程序

一些讀者可能想知道為什麼有人甚至嘗試使用Java寫實時程序。 每個人都知道Java不是實時平臺。 實際上普通Windows或Linux都不是實時操作系統。沒有人會用Java編寫真正的實時程序(例如自動駕駛儀)。 在本文中,實時程序是指接近實時的程序(即軟實時程序):那些允許發生少量事件丟失的程序。距離來說比如網絡流量分析器,如果在一百萬個數據包中丟失一兩百個包,通常不是大問題。 這樣的程序幾乎可以用任何語言(包括Java)開發,並可以在常規操作系統上運行。 我們將使用這種分析器的極其簡模型作為示例程序。

GC的影響

為什麼在分配內存和內存池之間進行選擇非常重要?對於Java而言 ,最重要的因素是垃圾收集器( GC ),因為它確實可以暫停整個程序的執行(稱為“停止世界”)。

最簡單的形式的垃圾收集器:

真正的垃圾收集器採用各種技巧來提高性能,消除長時間停頓並降低對活動對象數量的敏感性:

  • Java

這些改進通常需要生成代碼,例如寫屏障,甚至讀屏障。這些都降低了執行速度,但在許多情況下,仍可通過減少垃圾收集暫停來證明其合理性。

但是,這些改進不會影響兩個基本的GC規則:分配更多內存時,GC調用頻率更高;而當存在更多活動對象時,GC運行時間更長。

總是分配新內存和內存池這兩種方法對GC的影響並不相同。分配新內存策略通常使活動對象的數量保持較小,但會導致GC被頻繁調用。內存池策略減少了內存分配,但所有緩衝區都是活動Java對象,導致GC的調用頻率較低,但運行時間更長。

我們在內存池版本中創建許多緩衝區的原因是,我們希望在短時間內同時使用多個緩衝區的。對於分配新內存的方案,這意味著頻繁且長期運行的GC。

顯然,緩衝區不是程序分配的唯一對象。一些程序保留了許多永久分配的數據結構(映射,緩存,事務日誌),相比之下緩衝區反而變得微不足道了。其他一些分配了如此多的臨時對象,從而使得分配緩衝區變得微不足道了。本文的例子不適用於這些情況。

其他問題

單獨進行內存分配會產生其他成本。通常,獲取新對象的地址很快(特別是對於採用線程本地內存池的Java實現)。然而,也將內存清零並調用構造函數等開銷。

另一方面,池化也涉及一些開銷。必須仔細跟蹤每個緩衝區的使用情況,以便一旦緩衝區變空就可以將其返回到空閒池。在多線程情況下,這可能會變得非常棘手。無法跟蹤緩衝區可能導致緩衝區洩漏,類似於C程序中的經典內存洩漏。

混合版本

一種常用的方法是保留一定容量的池,並在需求超出此容量時分配緩衝區。如果釋放的緩衝區未滿,則僅將其返回池中,否則將被丟棄。這種方法在池化和分配新內存之間提供了很好權衡,因此值得測試。

測試

我們將模擬網絡分析器,該程序從網絡接口捕獲數據包,解碼協議並收集統計信息。我們使用一個非常簡化的模型,該模型包括:

  • DirectByteBuffer

  • INTERNAL_QUEUE_SIZE

    ArrayDeque

  • STORED_COUNT

我們暫時先研究單線程的情況:處理程序和數據源將在同一線程中運行。稍後我們將考慮多線程情況。

為了減少代碼量,我們將以與池化方法相同的方式來實現混合解決方案,唯一的區別是池大小: MIX_POOL_SIZE或POOL_SIZE 。

我們將使用兩個數據源:

  • Java隊列,該線程每隔SOURCE_INTERVAL_NS納秒向隊列寫入時鐘信號(序列號)。接收到信號後,數據源將生成數據包。源隊列總容量將以毫秒為單位定義為MAX_CAPTURING_DELAY_MS 。如果在此時間段內不為隊列提供服務,則源數據包將丟失(此丟失將使用序列號檢測到)。在這裡,我們將關注數據包不會丟失的SOURCE_INTERVAL_NS(兩次包間隔時間)的最小值。我們假設MAX_CAPTURING_DELAY_MS

我們將對三種場景進行測試:

  • 場景A:接收數據包,解析並且丟棄大部分數據包。幾乎沒有保存在內存中。

  • 場景B:使用了大量的數據包,但仍然遠遠少於預先分配的內存;

  • 場景C:幾乎所有預分配的內存都將被使用。

這些情況對應於消息處理器的不同反應:

A :負載異常低, 或者,數據速率可能很高,但是大多數數據包被早期過濾掉,並且不會存儲在任何地方。

B :負載較為實際;在大多數情況下,這種情況是可以預期的;

C ; 負載異常高;我們不希望這種情況持續很長時間,但必須處理。這種情況是預先分配多緩衝區的原因。

這是我們使用的參數:

<table><tbody>VariableABC/<tbody><tbody>MAX_CAPTURING_DELAY_MS100
100100POOL_SIZE1,000,0001,000,0001,000,000MIX_POOL_SIZE200,000200,000200,000INTERNAL_QUEUE_SIZE10,000100,000
495,000STORED_COUNT10,000100,000495,000/<tbody>/<table>

源代碼在這裡(https://github.com/pzemtsov/article-allocate-or-pool)。

批量策略

為了衡量測試框架的成本,我們將引入另一種緩衝區分配策略:Dummy策略,其中我們只有一個數據包,其他地方都使用這一個數據包。

我們將在2.40GHz的雙Xeon®CPU E5-2620 v3上運行該程序,使用Linux內核版本為3.17.4和Java 的版本為1.8.142並使用2G堆內存。使用如下JVM參數:

# java -Xloggc:gclog -Xms2g -Xmx2g -server Main X [strategy] batch

測試結果(以納秒為單位):

<table><tbody>策略ABC/<tbody><tbody>Dummy595766Allocation4006854042Mix108
315466Pooling346470415/<tbody>/<table>

到目前為止,分配內存策略是最差的(在C場景下很糟糕),這似乎回答了我們的問題,池化模式更加合適,混合策略在A和B情況下是最好的,而在C場景下則稍差一些,這使其成為批處理的理想策略。

測試代碼跑得飛快(60 ns),內存分配,清零和垃圾回收拖慢了速度。

導致此測試性能下降的三個可能因素是:頻繁內存分配,頻繁垃圾回收和高垃圾回收成本。 分配內存策略在C場景下同時受到這三個方面的影響;難怪它的表現如此悲摧。

在A場景中,我們看到了頻繁但快速的GC與罕見但緩慢的GC之間的競爭(在第一種選擇中增加了分配和清零成本)。罕見但緩慢的GC贏了。

當我們查看垃圾收集統計信息時,總體情況就變得不那麼樂觀了,池化策略的優勢也變得不那麼明顯了。讓我們看一下這些文件。它們都包含大量有關GC暫停的記錄,只是其持續時間,頻率和類型(不同。以下是這些文件的分析結果:

<table><tbody>CaseStrategyMax GC pause, msAvg GC pause, msGC count / secGC fractionObject count, milGC time / object, ns/<tbody><tbody>A Allocation4494.5
4%0.045194Mix3561.91%0.63910Pooling940823
0.867%3.039271B Allocation176664.530%1.134
58Mix63400.83%1.13434Pooling9117120.6
40%3.534201C Allocation8663652.389%5.45467Mix
7904880.627%5.47889Pooling5764460.629%5.508
81/<tbody>/<table>

這裡的“ GC計數”是平均每秒GC調用次數,“ GC百分比”是在執行GC花費的時間的百分比。

根據GC的暫停數據,在進行實時操作時,池化策略實際上是最差的一種。它根本行不通,幾乎沒有任何顯示一秒鐘的行為以視為實時。實際上我們的策略針對場景C都不工作。

C場景下使用分配內存策略非常糟糕:它花費了89%的時間在GC上。在分配內存和清理內存上花費了很多時間。然而池化模式也可能很糟糕(情況A佔67%)。目前尚不清楚為什麼A中的GC負載比B和C中的GC負載重得多。

出於好奇,我測量了活動對象數量和每個對象平均GC時間(最後兩列)。GC時間與活動對象數量並不完全成正比,但總的來活動對象數據量高則回收速度慢。回收每個對象的時間驚人地長。回收每個對象大約100納秒,回收100萬個對象就消耗100毫秒,而一百萬個對象實際上並不多。大多數現實的Java程序更加複雜, 內存中有更多(數億)的對象。 這些程序在使用CMS垃圾收集器的時候無法實時運行。

實時測試

對於分配內存策略和A場景和源間隔為1000 ns的實時測試,這是參數和結果:

<code># java -Djava.library.path=. -Xloggc:gclog -Xms2g -Xmx2g \\/<code><code> -server Main A alloc 1000/<code><code>Test: ALLOC/<code><code>Input queue size: 100000/<code><code>Input queue capacity, ms: 99.99999999999999/<code><code>Internal queue: 1000 = 1 ms/<code><code>Stored: 1000/<code><code> 6.0; 6.0; lost: 0/<code><code> 7.0; 1.0; lost: 0/<code><code> 8.0; 1.0; lost: 5717/<code><code> 9.0; 1.0; lost: 0/<code><code> 10.1; 1.0; lost: 0/<code><code> 11.0; 1.0; lost: 0/<code><code> 12.0; 1.0; lost: 0/<code>

沒有任何數據包丟失,這意味著測試程序可以處理負載(我們可以忍受初始性能不足)。

隨著傳入的數據包速率增加, 結果逐步惡化。在500 ns時,我們在27秒後丟棄了約80K數據包,此後再無丟棄。300 ns的輸出如下所示:

<code>5.5; 5.5; lost: 279184/<code><code> 5.8; 0.3; lost: 113569/<code><code> 6.2; 0.3; lost: 111238/<code><code> 6.5; 0.4; lost: 228014/<code><code> 6.9; 0.3; lost: 143214/<code><code> 7.5; 0.6; lost: 296348/<code><code> 8.1; 0.6; lost: 1334374/<code>

實驗表明,不丟失數據包的最小延遲為400 ns(2.5M數據包/秒),與批處理結果非常匹配。

現在讓我們看一下池化策略:

<code># java -Djava.library.path=. -Xloggc:gclog -Xms2g -Xmx2g \\/<code><code> -server Main A pool 1000/<code><code>Test: POOL, size = 1000000/<code><code>Input queue size: 100000/<code><code>Input queue capacity, ms: 99.99999999999999/<code><code>Internal queue: 1000 = 1 ms/<code><code>Stored: 1000/<code><code> 6.0; 6.0; lost: 0/<code><code> 7.0; 1.0; lost: 0/<code><code> 8.0; 1.0; lost: 0/<code><code> 10.3; 2.3; lost: 1250212/<code><code> 11.3; 1.0; lost: 0/<code><code> 12.3; 1.0; lost: 0/<code><code> 13.3; 1.0; lost: 0/<code><code> 15.0; 1.8; lost: 756910/<code><code> 16.0; 1.0; lost: 0/<code><code> 17.0; 1.0; lost: 0/<code><code> 18.0; 1.0; lost: 0/<code><code> 19.8; 1.8; lost: 768783/<code>

這是我們從批處理測試結果中得出的預測:因為其GC暫停時間長於輸入隊列容量,合併數據包處理器將無法處理負載。快速瀏覽gclog文件會發現暫停與批處理測試中的暫停(大約800毫秒)相同,GC大約每四秒鐘運行一次。

無論我們做什麼,池化策略都無法處理情況A ,更不用說B或C了 。增加堆大小會降低GC的頻率,但不會影響其持續時間。增加 源數據包間隔也無濟於事,例如,即使數據包間隔10,000 ns,每40秒也會丟失約80K數據包。將源隊列的容量增加到GC暫停(一秒或更長時間)以上的某個值才能緩解,但這顯然也是有問題的。

這是所有測試的合併結果。使用以下圖例:

<table><tbody>StrategyABC/<tbody><tbody>Allocation600lost: 0.8% (0.3%)lost: 75% (20%)Mix
150
350lost: 9% (0.6%)Poolinglost: 17% (0.5%)lost: 17% (0.5%)lost: 9% (0.6%)/<tbody>/<table>

請注意,內存池用於處理C場景。 相同的池,但針對B場景的大小稱為“mix”,並且效果很好。這意味著,對於我們可以處理的情況,池化策略仍比分配內存策略更好,而在某些情況下無法處理。

增加堆大小可以將損失減少到幾乎可以承受的程度,並“幾乎解決”了該問題。如人們所料,它在池化策略的情況下效果更好。然而這種方法看起來很荒謬:誰想使用10 Gb RAM而不是2 Gb只是為了將丟包率從17%減少到0.5%?

G1垃圾收集器

到目前為止,我們一直在使用CMS垃圾收集器。G1(“垃圾優先”)收集器。,在Java 9中成為事實標準,但在Java 8中也可以使用。該垃圾收集器對實時性要求較高的場景更加友好。例如,可以在命令行中指定允許的最大GC暫停時間。因此讓我們使用G1重複測試。

這是批處理測試的命令行參數:

<code>java -Xloggc:gclog -Xms2g -Xmx2g -XX:+UseG1GC -XX:MaxGCPauseMillis=80 \\ -server Main alloc batch/<code>

以下是批處理測試的結果(圖例:G1時間/ CMS時間):

<table><tbody>StrategyABC/<tbody><tbody>Dummy78 / 5970 / 5781 / 66
Allocation424 / 400640 / 6854300 / 4042Mix134 / 108364 / 315625 / 466Pooling140 / 346355 / 470
740 / 415/<tbody>/<table>

在大多數情況下,執行速度會變慢,在10%到130%之間,但在情況A和B中,池化策略速度更快。

分析垃圾收集器日誌。現在更加複雜了,因為G1日誌中的每一行並非都表示暫停。有些表示異步操作,實際不會停止程序執行。

<table><tbody>CaseStrategyMax GC pause, msAvg GC pause, msGC count / secGC fractionObject count, milGC time / object, ns/<tbody><tbody>A Allocation56
202.45%0.045444Mix43240.51%0.63938
Pooling47211.33%3.0397B Allocation85485.8
28%1.13442Mix81650.32%1.13457Pooling76
620.63%3.53417C Allocation7321182.428%5.454
21Mix1721102.325%5.47820Pooling1731172.0
23%5.50821/<tbody>/<table>

結果看起來比CMS更好,並有望為B場景提供可行的解決方案。讓我們運行實時測試:

<table><tbody>StrategyABC/<tbody><tbody>Allocation7502000lost: 76% (13%)Mix200
600lost: 4% (1%)Pooling200600lost: 4.4% (0.8%)/<tbody>/<table>

G1收集器的影響參差不齊,然而與傳統CMS相比,這樣做的性能要差得多。G1並不是解決所有問題的銀彈:對於C場景我們仍然沒有解決方案。

池化策略仍然比分配內存策略更好。

ZGC

我們從Java 8直接跳到Java 11 ,它具有一個全新的垃圾收集器ZGC,號稱能夠處理TB級的堆和億萬個對象。

在撰寫本文時,此垃圾收集器僅在Linux上可用,並且僅作為實驗性功能。讓我們吃個螃蟹。

命令行如下所示:

<code>java -Xloggc:gclog -Xms2g -Xmx2g -XX:+UnlockExperimentalVMOptions -XX:+UseZGC -server Main A alloc batch/<code>

以下是批處理測試結果(圖例為ZGC時間/ G1時間):

<table><tbody>StrategyABC/<tbody><tbody>Dummy72 / 7866 / 7084 / 81Allocation523 / 424800 / 640
1880 / 4300
Mix108 / 134403 / 364436 / 625Pooling109 / 140403 / 355453 / 740/<tbody>/<table>

在某些情況下,性能會有所下降,而在大部分些情況下,性能會有所提高。ZGC確實比以前的GC更好。

我沒有找到帶有暫停時間的完整ZGC日誌轉儲的JVM命令行參數,因此我暫時跳過這部分。這是ZGC的實時測試結果:

<table><tbody>StrategyABC/<tbody><tbody>Allocation540820lost: 44% (1.7%)Mix120420450Pooling130
420460/<tbody>/<table>

所有場景的結果都不錯,可以說處理一個數據包需要450 ns太多了(每秒只處理200萬個數據包),然而即使如此我們以前也做不到。其他場景的數字也不錯。池化策略看起來仍然比分配內存策略好。

使用預先分配本機緩衝區的CMS

儘管ZGC似乎可以解決我們的問題,但我們不想就此罷休。畢竟,它仍然是試驗性的。如果我們可以提高傳統垃圾收集器的性能呢?ZGC是否可以進一步提高吞吐量?

對於傳統的收集器,觀察到的GC性能似乎有點低,每個對象的延遲似乎很高。為什麼會這樣?一個想法是我們的內存分配模式與GC所調整的模式不同。Java程序會隨時分配對象,通常它們分配“普通”對象(多個字段的結構),而不是大數組。

我們將這些緩衝區移出堆並使堆變小。我們使用DirectByteBuffer在堆外內存中分配它們。分配DirectByteBuffer的代價也是相當高昂的(除其他事項外,它還會調用System.gc() ),並且釋放內存也不簡單 。這就是為什麼在我們的分配內存版本和池化版本中,我們都將這些緩衝區池化,並且我們將在堆外進行。除此之外,分配內存版本將在每次需要它們時分配數據包對象,而池化版本會將它們保留在集合中。儘管數據包的數量與以前相同,但是對象的總數會減少,因為以前我們有byte buffer和byte array,而現在我們只有byte buffer。

也可以說,“分配內存”策略現在不再是真正的“分配”:我們仍然必須為本機緩衝區實現某種池化方案。但我們仍然會測試其性能。

讓我們從CMS GC(批處理測試)開始。這是命令行:

<code> java -Xloggc:gclog -Xms1g -Xmx1g -XX:MaxDirectMemorySize=2g -server \\ Main A native-alloc batch/<code>

Java堆的大小已減少到1 GB。

這是批處理結果:

<table><tbody>StrategyABC/<tbody><tbody>Dummy505358Allocation
89253950Mix83221298Pooling79213260/<tbody>/<table>

結果(除分配內存策略在C場景情況下 )看起來非常好,並且所有結果都比我們到目前為止所看到的要好得多。這似乎是批處理的理想選擇。

讓我們看一下實時結果:

<table><tbody>StrategyABC/<tbody><tbody>Allocation140lost: 0.8%lost: 34%Mix130250; lost: 0.0025%lost: 0.7%
Pooling120300; lost: 0.03%lost: 0.7%/<tbody>/<table>

注意新的符號:“ 250; 丟失:0.0025%”表示,儘管我們仍然丟失數據包,但損耗很小,足以引發最小適用間隔的問題。簡而言之,這是一個“幾乎可行的”解決方案。

池化策略在C場景的GC日誌如下所示:

<code>60.618: [GC (Allocation Failure) 953302K->700246K(1010688K), 0.0720599 secs]/<code><code>62.457: [GC (Allocation Failure) 973142K->717526K(1010176K), 0.0583657 secs]/<code><code>62.515: [Full GC (Ergonomics) 717526K->192907K(1010176K), 0.4102448 secs]/<code><code>64.652: [GC (Allocation Failure) 465803K->220331K(1011712K), 0.0403231 secs]/<code>

大約每兩秒鐘就會有一次短暫的GC運行,收集大約200MB內存,但每次仍會增加20MB的內存使用量。最終會內存不足,每60秒就會有一個400毫秒的GC,將導致大約35萬個數據包丟棄。

“ B ”場景甚至更好:FULL GC僅每1100秒出現一次,大約相當於丟棄總數據包的0.03%(一百萬箇中的300個)。對於混合方案而言更是如此。這樣甚至可以在生產環境中使用該解決方案。

本地緩衝區,G1

這是批處理結果:

結果比沒有本地緩衝區要好,但比cms批處理結果差。

<table><tbody>Strategy Case ACase BCase CDummy 62 63 79
Allocation 108 239 1100 Mix 117 246 432 Pooling 111 249 347

/<tbody>/<table>

實時測試的結果:

<table><tbody>StrategyABC/<tbody><tbody>Allocation150350lost: 6.5%Mix150400800; lost: 0.075%
Pooling160500700/<tbody>/<table>

雖然看起來比a場景下cms結果差一點,但是依然有進步。

本地緩衝區,ZGC

現在讓我們在批處理測試中嘗試ZGC(將結果與沒有本地緩衝區的ZGC結果進行比較):

<table><tbody>StrategyABC/<tbody><tbody>DUMMY63/7276/66
102/84127/523290/800533/ 1880Mix100/108290/403400/436118/109302/403
330/453/<tbody>/<table>

幾乎所有場景都有明顯的改進,尤其是在分配內存策略測試中。但是G1,尤其是CMS的結果仍然好得多。

最後,這是實時測試結果:

<table><tbody>StrategyABC/<tbody><tbody>170380550Mix120
320440130320460/<tbody>/<table>

現在我們為所有策略和所有場景提供了一個可行的解決方案。甚至在C場景分配內存策略的情況下都可以使用。

嘗試C ++

我們已經看到內存管理確實影響Java程序的性能。我們可以嘗試通過使用自己的堆外內存管理器來減少這些開銷(我將在以下文章之一中探討這種技術)。 然而我們也可以嘗試用C ++來寫。

C ++中不存在垃圾回收問題;我們可以根據需要保留儘可能多的活動對象,不會引起任何暫停。它可能會由於緩存性能差而降低性能,但這是另一回事。

這使得分配內存策略和池化策略之間的選擇顯而易見:無論分配內存的成本多麼小,池化的成本均為零。因此,池化必將獲勝。讓我們測試一下。

我們的第一個版本將是Java版本的直接翻譯,具有相同的設計特性。具體來說,我們將在需要時分配ipheader和ipv4address對象。這使得dummy版本洩漏內存,因為同一個緩衝區對象多次重複使用而不返回池中,並且沒有人在過程中刪除這些對象。

這是批處理結果:

<table><tbody>StrategyA
BC/<tbody><tbody>Dummy145164
164270560616Mix
115223307Pooling111
233274/<tbody>/<table>

結果看起來不錯,但令人驚訝的是,效果並不理想。在使用Java的本地緩衝區+CMS解決方案中,我們已經得到了更好的結果。其他一些組合,Java版的結果也更好。分配內存策略的結果與Java中的大多數結果一樣糟糕,而且令人驚訝的是,dummy的結果也很糟糕。這表明內存分配在C ++中非常昂貴,即使沒有GC也比Java中昂貴得多。

以下是實時測試的結果:

<table><tbody>StrategyABC/<tbody><tbody>Allocation520950
950Mix280320550Pooling250420480/<tbody>/<table>

結果看起來不錯(至少涵蓋了所有情況),但是使用ZGC和本機緩衝區的Java數字起來更好。使用C++的方法必須儘可能減少內存分配。

C ++:無分配

以前的解決方案是以Java方式實現的:在需要時分配一個對象(例如IPv4A

ddress )。在Java中 我們別無選擇,但是在C ++中,我們可以在緩衝區內為最常用的對象保留內存。這將導致在分組處理期間將內存分配減少到零。我們將其稱為flat C ++版本。

這是批處理結果:

<table><tbody>StrategyABC/<tbody><tbody>Dummy161616163409
480Mix35153184Pooling34148171/<tbody>/<table>

所有這些結果都比對應的Java測試要好得多。從絕對意義上講,mix和池化也非常好。

實時測試結果如下所示:

<table><tbody>StrategyABC/<tbody><tbody>Allocation220650700Mix50220240Pooling
50190230/<tbody>/<table>

某些Java版本為分配內存策略提供了更好的結果。本機ZGC在C場景下甚至表現更好,這可以歸因於C ++內存管理器的緩慢和不可預測的特性。但是,其他版本的性能都很好。池化版本在C場景下每秒可以處理400萬個數據包,在B場景下每秒可以處理500萬個數據包,可以達到我們的期望值。A場景的處理速度絕對是驚人的(兩千萬),但是我們必須記住,在這種情況下,我們會丟棄這些數據包。

由於在池化過程中根本不執行任何內存分配,因此場景A , B和C之間的速度差異只能由已用內存的總容量不同來解釋–所用內存更多和隨機訪問模式會降低緩存效率。

彙總

讓我們將所有結果彙總在一個表中。我們將忽略dummpy的結果以及使用高得離譜的堆內存大小獲得的結果。

讓我們首先看一下批處理測試:

<table><tbody>Solution Strategy Case ACase BCase CCMS Allocation 400 685 4042 Mix 108 315
466 Pooling 346 470 415 G1 Allocation 424 640 4300 Mix 134
364 625 Pooling 140 355 740 ZGC Allocation 523 800 1880
Mix 108 403 436 Pooling 109 403 453 Native CMS Allocation 89 253 950
Mix 83 221 298 Pooling 79 213 260 Native G1 Allocation 108 239 1100
Mix 117 246 432 Pooling 111 249 347 Native ZGC Allocation 127 290 533
Mix 100 290 400 Pooling 118 302 330 C++ Allocation 270 560 616
Mix 115 223 307 Pooling 111 233 274 C++ flat Allocation 163 409 480
Mix 35 153 184 Pooling 34 148 171 /<tbody>/<table>

每列中的絕對最佳結果被標記為綠色,並且所有這三個都恰好來自flat C ++ 。

最佳和次佳Java結果分別標記為黃色和紅色。它們來自“ Native CMS”,這表明CMS垃圾收集器距離退役為時尚早。它仍然可以很好地用於批處理程序。

最後,這是實時測試的主要結果:

<table><tbody>Strategy Solution Case ACase BCase CCMS Allocation 600 lost: 0.8% lost: 75%Mix 150
350 lost: 9%Pooling lost: 17%lost: 17% lost: 9 G1 Allocation 750 2000 lost: 76% Mix 200
600 lost: 4% Pooling 200 600 lost: 4.4% ZGC Allocation 540 820 lost: 44% Mix 120
420 450 Pooling 130 420 460 Native CMS Allocation 140 lost: 0.8% lost: 34% Mix
130 lost: 0.0025% lost: 0.7% Pooling 120 lost: 0.03% lost: 0.7% Native G1 Allocation 150 350 lost: 6.5%
Mix 150 400 lost: 0.075% Pooling 160 500 700 Native ZGC Allocation 170 380
550 Mix 120 320 440 Pooling 130 320 460 C++ Allocation 520 950
950 Mix 280 320 550 Pooling 250 420 480 C++ flat Allocation 220 650
700 Mix 50 220 240 Pooling 50 190 230

/<tbody>/<table>

深灰色塊表示缺少解決方案(數據包始終丟失)。否則,配色方案相同。flat C ++版本依然是最好的,而最好的和次之的Java版本則來自多個解決方案,最好的是Native ZGC。

結論

如果要編寫真正的實時系統,請使用C或C ++編寫,並避免分配內存。也可以在Java中實現一些相當不錯的實時近似。在這種情況下,它也有助於減少內存分配。

這回答了我們最初的問題(分配內存或池化):池化。 在我們運行的每個測試中,池化的性能要好於分配內存。此外,在大多數Java測試中,分配內存策略在批處理模式下執行得很糟糕,而在實時模式下根本無法執行。

當數據包利用率低時,混合方法非常好。但是,如果利用率增長,則池化變得更好。

垃圾收集器確實是最大的影響因素。池化會引入很多活動對象,這些活動對象會導致偶發但很長的GC延遲。然而分配內存策略會使GC完全過載。此外,在高負載時(我們的C場景 ),無論如何可能存在許多活動對象,並且分配內存策略表現很慘。因此池化仍然是更好的策略。

G1和ZGC收集器儘管經常在批處理模式下表現較差,但它們確實在實時模式下有所改善。ZGC表現特別出色;它甚至可以以合理的性能(每秒200萬個數據包)處理C場景。

如果我們分配一千萬個緩衝區而不是一百萬個緩衝區,或者如果程序使用其他大數據結構,一切都會變得更糟。一種可能的解決方案是將這些結構移到堆外。

在不需要立即響應傳入消息的情況下,增加輸入隊列大小可能會有所幫助。我們可以考慮在C場景下引入另一層以及更高容量的中間隊列。如果我們的源隊列中可以存儲一秒鐘的數據包,則即使使用CMS,池化版本也可以正常工作。

原文地址:

https://pzemtsov.github.io/2019/01/17/allocate-or-pool.html

參考閱讀:

  • 一種靈活的API設計模式:在Spring Boot中支持GraphQL

  • 支付核心系統設計:Airbnb的分佈式事務方案簡介

  • 算力提升117%,資源使用下降50%,打開集群優化正確姿勢

  • Golang實現單機百萬長連接服務 - 美圖的三年優化經驗

  • 幾款流行監控系統簡介

高可用架構

改變互聯網的構建方式


分享到:


相關文章: