Büyük Nesne Yığını Parçalama


97

Üzerinde çalıştığım C # /. NET uygulamasında yavaş bellek sızıntısı var. Ne olduğunu belirlemeye çalışmak için SOS ile CDB'yi kullandım, ancak veriler herhangi bir anlam ifade etmiyor, bu yüzden birinizin bunu daha önce yaşamış olabileceğini umuyordum.

Uygulama 64 bit çerçevede çalışıyor. Verileri sürekli olarak hesaplıyor ve uzaktaki bir ana bilgisayara serileştiriyor ve Büyük Nesne Yığını'na (LOH) biraz isabet ediyor. Bununla birlikte, geçici olmasını beklediğim LOH nesnelerinin çoğu: hesaplama tamamlandıktan ve uzak ana bilgisayara gönderildikten sonra, bellek serbest bırakılmalıdır. Bununla birlikte, gördüğüm şey, boş bellek blokları ile serpiştirilmiş çok sayıda (canlı) nesne dizisi, örneğin, LOH'den rastgele bir segment alarak:

0:000> !DumpHeap 000000005b5b1000  000000006351da10
         Address               MT     Size
...
000000005d4f92e0 0000064280c7c970 16147872
000000005e45f880 00000000001661d0  1901752 Free
000000005e62fd38 00000642788d8ba8     1056       <--
000000005e630158 00000000001661d0  5988848 Free
000000005ebe6348 00000642788d8ba8     1056
000000005ebe6768 00000000001661d0  6481336 Free
000000005f214d20 00000642788d8ba8     1056
000000005f215140 00000000001661d0  7346016 Free
000000005f9168a0 00000642788d8ba8     1056
000000005f916cc0 00000000001661d0  7611648 Free
00000000600591c0 00000642788d8ba8     1056
00000000600595e0 00000000001661d0   264808 Free
...

Açıkçası, uygulamam her hesaplama sırasında uzun ömürlü, büyük nesneler oluşturuyorsa durumun böyle olmasını beklerdim. (Bunu yapıyor ve bir dereceye kadar LOH parçalanması olacağını kabul ediyorum, ancak buradaki sorun bu değil.) Sorun, kodda göremediğim yukarıdaki dökümde görebileceğiniz çok küçük (1056 bayt) nesne dizileridir. yaratılıyor ve bir şekilde köklü kalıyor.

Ayrıca, CDB'nin yığın segmenti döküldüğünde türü rapor etmediğini unutmayın: Bunun ilişkili olup olmadığından emin değilim. İşaretli (<-) nesneyi dökersem, CDB / SOS bunu iyi bildirir:

0:015> !DumpObj 000000005e62fd38
Name: System.Object[]
MethodTable: 00000642788d8ba8
EEClass: 00000642789d7660
Size: 1056(0x420) bytes
Array: Rank 1, Number of elements 128, Type CLASS
Element Type: System.Object
Fields:
None

Nesne dizisinin öğelerinin tümü dizelerdir ve dizeler uygulama kodumuzdan olduğu gibi tanınabilir.

Ayrıca,! GCRoot komutu askıda kaldığı ve asla geri dönmediği için GC köklerini bulamıyorum (hatta bir gecede bırakmayı denedim).

Bu nedenle, bu küçük (<85k) nesne dizilerinin neden LOH'da sona erdiğine dair bir ışık tutabilirse çok memnun olurum: .NET oraya hangi durumlarda küçük bir nesne dizisi koyar? Ayrıca, bu nesnelerin köklerini tespit etmenin alternatif bir yolunu bilen var mı?


Güncelleme 1

Dün geç saatlerde ortaya attığım bir başka teori, bu nesne dizilerinin büyük başladığını, ancak bellek dökümlerinde belirgin olan boş bellek bloklarını bırakarak küçüldükleri. Beni şüphelendiren şey, nesne dizilerinin her zaman 1056 bayt uzunluğunda (128 öğe), referanslar için 128 * 8 ve 32 bayt ek yük olarak görünmesidir.

Buradaki fikir, bir kitaplıktaki veya CLR'deki bazı güvenli olmayan kodların dizi başlığındaki öğe sayısını bozmasıdır. Biliyorum biraz uzun bir atış ...


Güncelleme 2

Brian Rasmussen sayesinde (kabul edilen cevaba bakın) sorun, dizeli stajyer tablosunun neden olduğu LOH'nin parçalanması olarak tanımlandı! Bunu onaylamak için hızlı bir test uygulaması yazdım:

static void Main()
{
    const int ITERATIONS = 100000;

    for (int index = 0; index < ITERATIONS; ++index)
    {
        string str = "NonInterned" + index;
        Console.Out.WriteLine(str);
    }

    Console.Out.WriteLine("Continue.");
    Console.In.ReadLine();

    for (int index = 0; index < ITERATIONS; ++index)
    {
        string str = string.Intern("Interned" + index);
        Console.Out.WriteLine(str);
    }

    Console.Out.WriteLine("Continue?");
    Console.In.ReadLine();
}

Uygulama önce bir döngüde benzersiz dizeler oluşturur ve bunlardan vazgeçer. Bu sadece hafızanın bu senaryoda sızmadığını kanıtlamak içindir. Açıkçası olmamalı ve olmuyor.

İkinci döngüde benzersiz dizeler oluşturulur ve dahil edilir. Bu eylem onları stajyer masasına yerleştirir. Anlayamadığım şey stajyer masasının nasıl temsil edildiği. Görünüşe göre, LOH'da oluşturulan bir dizi sayfadan - 128 dize öğesinden oluşan nesne dizileri - oluşur. Bu, CDB / SOS'ta daha belirgindir:

0:000> .loadby sos mscorwks
0:000> !EEHeap -gc
Number of GC Heaps: 1
generation 0 starts at 0x00f7a9b0
generation 1 starts at 0x00e79c3c
generation 2 starts at 0x00b21000
ephemeral segment allocation context: none
 segment    begin allocated     size
00b20000 00b21000  010029bc 0x004e19bc(5118396)
Large object heap starts at 0x01b21000
 segment    begin allocated     size
01b20000 01b21000  01b8ade0 0x00069de0(433632)
Total Size  0x54b79c(5552028)
------------------------------
GC Heap Size  0x54b79c(5552028)

LOH segmentinin bir dökümünü almak, sızıntı uygulamasında gördüğüm modeli ortaya çıkarır:

0:000> !DumpHeap 01b21000 01b8ade0
...
01b8a120 793040bc      528
01b8a330 00175e88       16 Free
01b8a340 793040bc      528
01b8a550 00175e88       16 Free
01b8a560 793040bc      528
01b8a770 00175e88       16 Free
01b8a780 793040bc      528
01b8a990 00175e88       16 Free
01b8a9a0 793040bc      528
01b8abb0 00175e88       16 Free
01b8abc0 793040bc      528
01b8add0 00175e88       16 Free    total 1568 objects
Statistics:
      MT    Count    TotalSize Class Name
00175e88      784        12544      Free
793040bc      784       421088 System.Object[]
Total 1568 objects

İş istasyonum 32 bit ve uygulama sunucusu 64 bit olduğundan nesne dizisi boyutunun 528 (1056 yerine) olduğunu unutmayın. Nesne dizileri hala 128 eleman uzunluğundadır.

Yani bu hikayenin ahlaki, stajyerlik yaparken çok dikkatli olmaktır. İnterneti yaptığınız dizinin sonlu bir kümenin üyesi olduğu bilinmiyorsa, en azından CLR sürüm 2'de LOH'nin parçalanması nedeniyle uygulamanız sızacaktır.

Bizim uygulamamızın durumunda, serileştirme kod yolunda, unmarshalling sırasında varlık tanımlayıcılarını stajyerleştiren genel kod vardır: Şimdi bunun suçlu olduğundan kesinlikle şüpheleniyorum. Bununla birlikte, geliştiricinin niyetleri açıkça iyiydi çünkü aynı varlık birden çok kez serileştirilmişse, tanımlayıcı dizesinin yalnızca bir örneğinin bellekte tutulacağından emin olmak istiyorlardı.


2
Harika soru - başvurumda da aynı şeyi fark ettim. Büyük bloklar temizlendikten sonra LOH'de kalan küçük nesneler ve parçalanma sorunlarına neden olur.
Reed Copsey

2
Katılıyorum, harika soru. Cevapları izleyeceğim.
Charlie Flowers

2
Çok ilginç. Görünüşe göre hata ayıklamak büyük bir sorunmuş!
Matt Jordan

Yanıtlar:


47

CLR, birkaç nesneyi önceden tahsis etmek için LOH kullanır ( dahili dizeler için kullanılan dizi gibi ). Bunların bazıları 85000 bayttan azdır ve bu nedenle normalde LOH'ye tahsis edilmez.

Bu bir uygulama ayrıntısıdır, ancak bunun nedeninin, süreç kendisi olduğu sürece hayatta kalması gereken örneklerin gereksiz çöp toplama işlemlerinden kaçınmak olduğunu varsayıyorum.

Ayrıca biraz ezoterik bir optimizasyon nedeniyle double[], 1000 veya daha fazla unsurdan herhangi biri de LOH'ye tahsis edilir.


Sorunlu nesneler, uygulama kodu tarafından oluşturulduğunu bildiğim dizelerin referanslarını içeren nesnelerdir. Bu, uygulamanın [] nesnelerini oluşturduğunu (bunun kanıtını göremiyorum) veya CLR'nin bir kısmının (serileştirme gibi) bunları uygulama nesneleri üzerinde çalışmak için kullandığı anlamına gelir.
Paul Ruane

1
Bu, dahili dizeler için kullanılan iç yapı olabilir. Daha fazla ayrıntı için lütfen bu soru için cevabımı kontrol edin: stackoverflow.com/questions/372547/…
Brian Rasmussen

Ah, bu çok ilginç bir ipucu, teşekkürler. Staj masasını tamamen unutmuşum. Geliştiricilerimizden birinin istekli bir stajyer olduğunu biliyorum, bu yüzden bu kesinlikle araştırmam gereken bir şey.
Paul Ruane

1
85000 bayt veya 84 * 1024 = 87040 bayt?
Peter Mortensen

5
85000 bayt. 85000-12 bayt dizisi oluşturarak (uzunluk boyutu, MT, eşitleme bloğu) ve GC.GetGenerationörneği çağırarak bunu doğrulayabilirsiniz . Bu, Gen2'yi döndürecektir - API, Gen2 ve LOH arasında ayrım yapmaz. Diziyi bir bayt küçültün ve API Gen0'ı döndürür.
Brian Rasmussen

13

.NET Framework 4.5.1, çöp toplama sırasında büyük nesne yığınını (LOH) açıkça sıkıştırma yeteneğine sahiptir.

GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
GC.Collect();

GCSettings.LargeObjectHeapCompactionMode'da daha fazla bilgi görün


2

GC'nin nasıl çalıştığına ilişkin açıklamaları ve uzun ömürlü nesnelerin 2. nesilde ne kadar sona erdiği ve LOH nesnelerinin toplanması hakkındaki kısımları okurken, 2. nesil koleksiyonda olduğu gibi, akla gelen fikirdir. .. neden 2. nesil ve büyük nesneler bir araya toplanacakları için aynı yığın içinde tutulmasın?

Gerçekte olan buysa, küçük nesnelerin LOH ile aynı yere nasıl geldiklerini açıklar - eğer 2. nesilde son bulacak kadar uzun ömürlülerse.

Ve böylece sorununuz bana gelen fikri oldukça iyi bir şekilde çürütecek gibi görünecektir - bu, LOH'nin parçalanmasıyla sonuçlanacaktır.

Özet: Sorununuz olabilir bu açıklama hiçbir vasıta kanıtı tarafından olmasına rağmen, aynı yığın bölgesini paylaşan GBH ve kuşak 2 ile açıklanabilir.

Güncelleme: çıktısı !dumpheap -statbu teoriyi sudan çıkarıyor! 2. nesil ve LOH kendi bölgelerine sahiptir.


Her bir yığını oluşturan segmentleri göstermek için! Eeheap kullanın. Gen 0 ve gen 1 bir segmentte (aynı segment) yaşar, gen 2 ve LOH'nin her ikisi birden çok segmenti tahsis edebilir, ancak bu segmentler her yığın için ayrı kalır.
Paul Ruane

Evet, anladım, teşekkürler. Bu davranışı çok daha net bir şekilde gösterdiği için! Eeheaps komutundan bahsetmek istedim.
Paul Ruane

Ana GC'nin verimliliği, büyük ölçüde, nesneleri yeniden konumlandırabilmesinden kaynaklanmaktadır, böylece ana yığın üzerinde yalnızca az sayıda boş bellek bölgesi olacaktır. Bir koleksiyon sırasında ana öbek üzerindeki bir nesne sabitlenirse, sabitlenmiş nesnenin üstündeki ve altındaki alanın ayrı ayrı izlenmesi gerekebilir, ancak sabitlenmiş nesnelerin sayısı normalde çok küçük olduğundan, GC'nin yapması gereken ayrı alanların sayısı da olacaktır. Izlemek. Yeniden yerleştirilebilir ve yeniden yerleştirilemez (büyük) nesnelerin aynı yığın içinde karıştırılması performansı düşürür.
süperyat

Daha ilginç bir soru da .NET'in neden doubleGC'yi 8 baytlık sınırlar üzerinde hizalanmasını sağlamak için ince ayar yapmak yerine LOH üzerine 1000 öğeden daha büyük diziler koyduğudur. Aslında, 32 bitlik bir sistemde bile, önbellek davranışı nedeniyle, tahsis edilen boyutu 8 baytın katları olan tüm nesnelere 8 bayt hizalama uygulanmasının muhtemelen bir performans kazanımı olacağını umuyorum. Aksi takdirde, yoğun şekilde kullanılan double[]ve önbelleğe hizalanmış bir performansın performansı olmayan bir performansa göre daha iyi olsa da, boyutun neden kullanımla ilişkili olduğunu bilmiyorum.
supercat

@supercat Ayrıca, iki yığın da ayırmada çok farklı davranır. Ana yığın (şu anda) temelde ayırma modellerinde bir yığın - her zaman en üstte ayırır, herhangi bir boş alanı yok sayar - sıkıştırma geldiğinde, boş alanlar sıkıştırılır. Bu, tahsisi neredeyse hiç işlemsiz hale getirir ve veri yerelliğine yardımcı olur. Öte yandan, LOH'de tahsis etmek, malloc'un çalışma şekline benzer - tahsis ettiğiniz şeyi tutabilecek ve orada tahsis edebilecek ilk boş yeri bulacaktır. Büyük nesneler için olduğu için, veri konumu verilir ve ayırma cezası çok kötü değildir.
Luaan

1

Biçim uygulamanız olarak tanınıyorsa, neden bu dize biçimini oluşturan kodu tanımlamadınız? Birkaç olasılık varsa, hangi kod yolunun suçlu olduğunu bulmak için benzersiz veriler eklemeyi deneyin.

Dizilerin büyük serbest öğelerle serpiştirilmiş olması, başlangıçta eşleştirildiklerini veya en azından ilişkili olduklarını tahmin etmeme neden oluyor. Onları neyin oluşturduğunu ve ilişkili dizeleri bulmak için serbest bırakılan nesneleri tanımlamaya çalışın.

Bu dizeleri neyin oluşturduğunu belirledikten sonra, onları GC'ye tabi tutmaktan neyin alıkoyacağını anlamaya çalışın. Belki de günlüğe kaydetme veya benzeri bir amaç için unutulmuş veya kullanılmayan bir listede dolduruluyorlar.


DÜZENLEME: Şu an için bellek bölgesini ve belirli dizi boyutunu göz ardı edin: bir sızıntıya neden olmak için bu dizelerle ne yapıldığını bulun. İzlenecek daha az nesne varken, programınız bu dizeleri yalnızca bir veya iki kez oluşturduğunda veya değiştirdiğinde! GCRoot'u deneyin.


Dizeler, kolayca tanımlanabilen Kılavuzlar (kullandığımız) ve dize anahtarlarının bir karışımıdır. Nerede üretildiklerini görebiliyorum, ancak hiçbir zaman (doğrudan) nesne dizilerine eklenmiyorlar ve açıkça 128 öğe dizisi oluşturmuyoruz. Bununla birlikte, bu küçük diziler LOH'da olmamalıdır.
Paul Ruane

1

Harika soru, soruları okuyarak öğrendim.

Sanırım serileştirme kod yolunun diğer bir parçası da büyük nesne yığınını kullanıyor, dolayısıyla parçalanma. Tüm diziler AYNI zamanda stajyer olsaydı, iyi olacağını düşünüyorum.

.Net çöp toplayıcısının ne kadar iyi olduğu göz önüne alındığında, seriyi kaldırma kod yolunun normal dize nesnesi oluşturmasına izin vermek muhtemelen yeterince iyi olacaktır. İhtiyaç ispatlanana kadar daha karmaşık bir şey yapmayın.

En çok, gördüğünüz son birkaç dizgenin bir karma tablosunu tutmaya ve bunları yeniden kullanmaya bakardım. Karma tablo boyutunu sınırlayarak ve tabloyu oluşturduğunuzda boyutu ileterek çoğu parçalanmayı durdurabilirsiniz. Daha sonra, boyutunu sınırlandırmak için son zamanlarda görmediğiniz dizeleri karma tablodan kaldırmanın bir yoluna ihtiyacınız var. Ancak, serileştirme kod yolunun oluşturduğu dizeler kısa ömürlü olursa, bir şey olsa bile fazla bir kazanç elde edemezsiniz.


1

İşte tam tanımlamak için yollar çift çağrı yığını içinde LOH tahsisi.

Ve LOH parçalanmasını önlemek için Geniş nesne dizisini önceden tahsis edin ve bunları sabitleyin. Gerektiğinde bu nesneleri tekrar kullanın. LOH Fragmentation ile ilgili yazı . Bunun gibi bir şey, LOH parçalanmasını önlemeye yardımcı olabilir.


Buraya sabitlemenin neden yardımcı olacağını anlayamıyorum? LOH üzerindeki BTW büyük nesneler yine de GC tarafından taşınmaz. Yine de bir uygulama detayı.
user492238

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.