Milyonlarca küçük geçici nesne oluşturmak için en iyi uygulama


109

Milyonlarca küçük nesneyi oluşturmak (ve serbest bırakmak) için "en iyi uygulamalar" nelerdir?

Java'da bir satranç programı yazıyorum ve arama algoritması her olası hareket için tek bir "Hareket" nesnesi oluşturur ve nominal bir arama, saniyede bir milyondan fazla hareket nesnesini kolayca oluşturabilir. JVM GC, geliştirme sistemimdeki yükü kaldırabildi, ancak aşağıdakileri sağlayacak alternatif yaklaşımları keşfetmekle ilgileniyorum:

  1. Çöp toplamanın ek yükünü en aza indirin ve
  2. alt uç sistemler için en yüksek bellek ayak izini azaltır.

Nesnelerin büyük bir çoğunluğu çok kısa ömürlüdür, ancak üretilen hareketlerin yaklaşık% 1'i kalıcıdır ve kalıcı değer olarak geri döndürülür, bu nedenle herhangi bir havuzlama veya önbelleğe alma tekniği, belirli nesneleri yeniden kullanımdan hariç tutma yeteneği sağlamalıdır. .

Tamamen ayrıntılı bir örnek kod beklemiyorum, ancak daha fazla okuma / araştırma için önerileri veya benzer nitelikteki açık kaynaklı örnekleri takdir ediyorum.


11
Flyweight Pattern davanız için uygun olur mu? en.wikipedia.org/wiki/Flyweight_pattern
Roger Rowland

4
Onu bir nesne içinde sarmalamanız gerekiyor mu?
nhahtdh

1
Flyweight Pattern uygun değildir çünkü nesneler önemli ortak verileri paylaşmaz. Verileri bir nesnede kapsüllemeye gelince, ilkel bir nesneye sığmayacak kadar büyük, bu yüzden POJO'lara alternatifler arıyorum.
Humble Programmer

2
Şiddetle tavsiye edilen okuma: cs.virginia.edu/kim/publicity/pldi09tutorials/…
rkj

Yanıtlar:


47

Uygulamayı ayrıntılı çöp toplama ile çalıştırın:

java -verbose:gc

Ve ne zaman topladığını size söyleyecektir. Hızlı ve tam tarama olmak üzere iki tür tarama olacaktır.

[GC 325407K->83000K(776768K), 0.2300771 secs]
[GC 325816K->83372K(776768K), 0.2454258 secs]
[Full GC 267628K->83769K(776768K), 1.8479984 secs]

Ok, boyuttan önce ve sonradır.

Sadece GC yapıyor ve tam bir GC olmadığı sürece evde güvendesiniz. Normal GC, 'genç kuşakta' bir kopya toplayıcıdır, bu nedenle artık referans verilmeyen nesneler sadece unutulur, bu da tam olarak istediğiniz şeydir.

Java SE 6 HotSpot Virtual Machine Garbage Collection Tuning'i okumak muhtemelen yararlıdır.


Tam çöp toplamanın nadir olduğu bir nokta bulmaya çalışmak için Java yığın boyutuyla denemeler yapın. Java 7'de yeni G1 GC bazı durumlarda daha hızlıdır (ve bazılarında daha yavaştır).
Michael Shopsin

21

Sürüm 6'dan bu yana, JVM'nin sunucu modu bir kaçış analizi tekniği kullanmaktadır. Bunu kullanarak GC'den hep birlikte kaçınabilirsiniz.


1
Kaçış analizi genellikle hayal kırıklığına uğratır, JVM'nin ne yaptığınızı anlayıp anlamadığını kontrol etmeye değer.
Nitsan Wakart

2
Bu seçeneği kullanma deneyiminiz varsa: -XX: + PrintEscapeAnalysis ve -XX: + PrintEliminateAllocations. Bunu paylaşmak harika olur. Çünkü dürüstçe söylemiyorum.
Mikhail

bkz stackoverflow.com/questions/9032519/... Eğer JDK 7 için bir hata ayıklama yapı almak gerekir, ben ancak JDK 6 ile başarılı oldu bunu yapmadı ettik itiraf.
Nitsan Wakart

19

Pekala, burada birkaç soru var!

1 - Kısa ömürlü nesneler nasıl yönetilir?

Daha önce belirtildiği gibi, JVM, Zayıf Kuşak Hipotezini izlediği için çok büyük miktarda kısa ömürlü nesneyle mükemmel bir şekilde başa çıkabilir .

Ana belleğe (yığına) ulaşan nesnelerden bahsettiğimize dikkat edin. Bu her zaman böyle değildir. Oluşturduğunuz birçok nesne bir CPU kaydı bile bırakmaz. Örneğin, bu for-döngüsünü düşünün

for(int i=0, i<max, i++) {
  // stuff that implies i
}

Döngü açmayı düşünmeyelim (JVM'nin kodunuzda yoğun bir şekilde gerçekleştirdiği optimizasyonlar). Eşitse max, Integer.MAX_VALUEdöngünün yürütülmesi biraz zaman alabilir. Bununla birlikte, ideğişken döngü bloğundan asla kaçmayacaktır. Bu nedenle, JVM bu değişkeni bir CPU kaydına koyacak, düzenli olarak artıracak, ancak ana belleğe asla geri göndermeyecektir.

Bu nedenle, yalnızca yerel olarak kullanılıyorsa, milyonlarca nesne oluşturmak önemli değildir. Eden'de saklanmadan önce ölmüş olacaklar, bu yüzden Genel Sekreter onları fark etmeyecek.

2 - GC'nin ek yükünü azaltmak faydalı mı?

Her zamanki gibi duruma göre değişir.

Öncelikle, neler olup bittiğini net bir şekilde görebilmek için GC günlüğünü etkinleştirmelisiniz. İle etkinleştirebilirsiniz -Xloggc:gc.log -XX:+PrintGCDetails.

Uygulamanız bir GC döngüsünde çok fazla zaman harcıyorsa, o zaman evet, GC'yi ayarlayın, aksi takdirde gerçekten buna değmeyebilir.

Örneğin, her 100 ms'de 10 ms süren genç bir GC'niz varsa, zamanınızın% 10'unu GC'de geçirirsiniz ve saniyede 10 koleksiyonunuz olur (ki bu huuuuge). Böyle bir durumda, bu 10 GC / s hala orada olacağından, GC ayarına zaman harcamam.

3 - Biraz deneyim

Belirli bir sınıfın büyük bir miktarını oluşturan bir uygulamada benzer bir sorun yaşadım. GC günlüklerinde, uygulamanın oluşturulma oranının 3 GB / sn civarında olduğunu fark ettim, bu çok fazla (hadi ... her saniye 3 gigabayt veri mi?!).

Sorun: Oluşturulan çok fazla nesnenin neden olduğu çok fazla sık GC.

Benim durumumda, bir bellek profilleyici ekledim ve bir sınıfın tüm nesnelerimin büyük bir yüzdesini temsil ettiğini fark ettim. Bu sınıfın temelde bir nesneye sarılmış bir çift boole olduğunu bulmak için örnekleri izledim. Bu durumda iki çözüm mevcuttu:

  • Algoritmayı bir boole çifti döndürmeyecek şekilde yeniden çalışın, bunun yerine her booleanı ayrı ayrı döndüren iki yöntemim var

  • Yalnızca 4 farklı örnek olduğunu bilerek nesneleri önbelleğe alın

Uygulamaya en az etkisi olduğu ve tanıtımı kolay olduğu için ikincisini seçtim. İş parçacığı güvenli olmayan bir önbelleğe sahip bir fabrika kurmak dakikalarımı aldı (sonunda yalnızca 4 farklı örneğe sahip olacağım için iş parçacığı güvenliğine ihtiyacım yoktu).

Tahsisat oranı ve aynı şekilde genç GC sıklığı 1 GB / s'ye düştü (3'e bölünür).

Umarım yardımcı olur !


11

Sadece değerli nesneleriniz varsa (yani, diğer nesnelere referanslar yoksa) ve gerçekten ama gerçekten tonlarca demek istiyorum, ByteBuffersyerel bayt sıralamasıyla doğrudan kullanabilirsiniz [ikincisi önemlidir] ve birkaç yüz satıra ihtiyacınız var. Alıcı / ayarlayıcıları ayırmak / yeniden kullanmak için kod. Getters benzer görünüyorlong getQuantity(int tupleIndex){return buffer.getLong(tupleInex+QUANTITY_OFFSSET);}

Bu, yalnızca bir kez, yani büyük bir parça ayırdığınız ve ardından nesneleri kendiniz yönettiğiniz sürece, GC sorununu neredeyse tamamen çözecektir. Referanslar yerine, iletilmesi gereken sadece dizine (yani int) sahip olursunuz ByteBuffer. Hafızayı kendiniz de hizalamanız gerekebilir.

Teknik, kullanmak gibi hissederdi C and void*, ancak bazı ambalajlarla katlanılabilir. Performansın bir dezavantajı, derleyicinin onu ortadan kaldırmayı başaramadığını kontrol eden sınırlar olabilir. Başlıca bir artı, tupl'ları vektörler gibi işlerseniz, nesne başlığının olmaması bellek ayak izini de azaltır.

Bunun dışında, neredeyse tüm JVM'lerin genç nesli önemsiz bir şekilde öldüğünden ve tahsis maliyeti sadece bir gösterge darbesi olduğundan, böyle bir yaklaşıma ihtiyacınız olmayacak. finalBazı platformlarda (yani ARM / Power) bellek çitine ihtiyaç duydukları için alanları kullanırsanız, tahsis maliyeti biraz daha yüksek olabilir , ancak x86'da ücretsizdir.


8

GC'nin bir sorun olduğunu varsayarsak (diğerlerinin de belirttiği gibi) sizin için özel bir durum, yani büyük karmaşaya maruz kalan bir sınıf için kendi bellek yönetiminizi uygulayacaksınız. Nesne havuzuna bir şans verin, oldukça iyi çalıştığı durumlar gördüm. Nesne havuzlarını uygulamak iyi bilinen bir yoldur, bu nedenle burayı tekrar ziyaret etmeye gerek yoktur, şunlara dikkat edin:

  • çoklu iş parçacığı: iş parçacığı yerel havuzlarını kullanmak durumunuz için işe yarayabilir
  • yedekleme veri yapısı: kaldırmada iyi performans gösterdiğinden ve ayırma ek yükü olmadığından ArrayDeque kullanmayı düşünün
  • havuzunuzun boyutunu sınırlayın :)

Önce / sonra vb. Ölçün


6

Benzer bir sorunla karşılaştım. Öncelikle küçük nesnelerin boyutunu küçültmeye çalışın. Her nesne örneğinde bunlara referans veren bazı varsayılan alan değerleri tanıttık.

Örneğin, MouseEvent'in Point sınıfına bir başvurusu vardır. Noktaları yeni örnekler oluşturmak yerine önbelleğe aldık ve onlara referans verdik. Aynı şey, örneğin boş dizeler için.

Başka bir kaynak, bir int ile değiştirilen birden çok boole idi ve her boole için int'in yalnızca bir baytını kullanıyoruz.


Sadece ilgi çekici: Performans açısından size ne kazandırdı? Değişiklikten önce ve sonra başvurunuzun profilini çıkardınız mı ve öyleyse, sonuçlar ne oldu?
Axel

@Axel nesneler çok daha az bellek kullanır, bu nedenle GC çok sık çağrılmaz. Kesinlikle uygulamamızın profilini çıkardık, ancak geliştirilmiş hızın görsel etkisi bile vardı.
StanislavL

6

Bu senaryoyu bir süre önce bazı XML işleme kodlarıyla halletmiştim. Kendimi çok küçük (genellikle sadece bir dize) ve son derece kısa ömürlü (bir XPath kontrolünün başarısız olması eşleşme olmadığı anlamına geliyordu ) milyonlarca XML etiketi nesnesi oluştururken buldum .

Bazı ciddi testler yaptım ve yenilerini yapmak yerine atılan etiketlerin bir listesini kullanarak hızda yalnızca yaklaşık% 7'lik bir iyileştirme elde edebileceğim sonucuna vardım. Ancak, bir kez uygulandığında, ücretsiz kuyruğun çok büyük olursa onu budamak için bir mekanizmaya ihtiyaç duyduğunu fark ettim - bu, optimizasyonumu tamamen geçersiz kıldı, bu yüzden bir seçeneğe geçtim.

Özetle - muhtemelen buna değmez - ama bunu düşündüğünüzü görmekten memnunum, önemsediğinizi gösteriyor.


2

Bir satranç programı yazdığınızı düşünürsek, iyi performans için kullanabileceğiniz bazı özel teknikler vardır. Basit bir yaklaşım, büyük bir uzun (veya bayt) dizisi oluşturmak ve bunu bir yığın olarak ele almaktır. Hareket oluşturucunuz her hareket oluşturduğunda, birkaç sayıyı yığına iter, örneğin kareden kareye hareket edin. Arama ağacını değerlendirirken, hareketler patlayacak ve bir pano temsilini güncelleyeceksiniz.

Etkileyici güç istiyorsanız nesneleri kullanın. Hız istiyorsanız (bu durumda) yerel gidin.


1

Bu tür arama algoritmaları için kullandığım bir çözüm, yalnızca bir Move nesnesi oluşturmak, onu yeni hareketle değiştirmek ve ardından kapsamı terk etmeden önce hareketi geri almaktır. Muhtemelen her seferinde yalnızca bir hareketi analiz ediyor ve sonra en iyi hareketi bir yerde depoluyorsunuz.

Bu herhangi bir nedenle mümkün değilse ve en yüksek bellek kullanımını azaltmak istiyorsanız, bellek verimliliği hakkında iyi bir makale burada: http://www.cs.virginia.edu/kim/publicity/pldi09tutorials/memory-efficient-java- tutorial.pdf


Ölü bağlantı. O makale için başka bir kaynak var mı?
dnault

0

Milyonlarca nesnenizi oluşturun ve kodunuzu doğru şekilde yazın: bu nesnelere gereksiz referanslar saklamayın. GC kirli işi sizin için yapacak. Gerçekten GC'lenmiş olup olmadıklarını görmek için belirtildiği gibi ayrıntılı GC ile oynayabilirsiniz. Java, nesneler oluşturmak ve yayınlamakla ilgilidir. :)


1
Üzgünüm dostum, yaklaşımınıza katılmıyorum ... Herhangi bir programlama dili gibi Java da kendi kısıtlamaları dahilinde bir problemi çözmekle ilgilidir, eğer OP GC tarafından kısıtlanmışsa ona nasıl yardım ediyorsunuz?
Nitsan Wakart

1
Ona Java'nın gerçekte nasıl çalıştığını anlatıyorum. Milyonlarca geçici nesneye sahip olma durumundan kaçamıyorsa, en iyi tavsiye, geçici sınıfın hafif olması ve referansları artık tek bir adım değil, mümkün olan en kısa sürede yayınlamasını sağlamalıdır. Bir şey mi kaçırıyorum?
gyorgyabraham

Java, çöp oluşturmayı destekler ve sizin için temizler, bu kadar doğrudur. OP nesnelerin yaratılmasını engelleyemiyorsa ve GC'de geçirilen zamandan mutsuzsa, bu üzücü bir sondur. Benim itirazım, GC için daha fazla çalışma yapmanız için yaptığınız tavsiyeye, çünkü bu bir şekilde uygun Java.
Nitsan Wakart

0

Java'da yığın tahsisi ve kaçış analizi hakkında okumalısınız.

Çünkü bu konuya daha derin girerseniz, nesnelerinizin öbek üzerinde tahsis edilmediğini ve öbek üzerindeki nesneler gibi GC tarafından toplanmadıklarını görebilirsiniz.

Kaçış analizinin bir wikipedia açıklaması var, bunun Java'da nasıl çalıştığına dair bir örnek:

http://en.wikipedia.org/wiki/Escape_analysis


0

Büyük bir GC hayranı değilim, bu yüzden her zaman etrafından dolaşmanın yollarını bulmaya çalışıyorum. Bu durumda Nesne Havuzu modelini kullanmanızı öneririm :

Buradaki fikir, daha sonra yeniden kullanabilmeniz için onları bir yığın halinde depolayarak yeni nesneler oluşturmaktan kaçınmaktır.

Class MyPool
{
   LinkedList<Objects> stack;

   Object getObject(); // takes from stack, if it's empty creates new one
   Object returnObject(); // adds to stack
}

3
Havuzu küçük nesneler için kullanmak oldukça kötü bir fikirdir, önyüklemek için iş parçacığı başına bir havuza ihtiyacınız vardır (veya paylaşılan erişim herhangi bir performansı öldürür). Bu tür havuzlar ayrıca iyi bir çöp toplayıcıdan daha kötü performans gösterir. Son olarak: GC eşzamanlı kod / yapılarla uğraşırken bir nimettir - doğal olarak ABA sorunu olmadığı için birçok algoritmanın uygulanması önemli ölçüde daha kolaydır. Ref. eşzamanlı ortamda sayma en az bir atomik işlem + bellek çit gerektirir (
x86'da

1
Havuzdaki nesnelerin yönetimi , çöp toplayıcının çalışmasına izin vermekten daha pahalı olabilir .
Thorbjørn Ravn Andersen

@ ThorbjørnRavnAndersen Genel olarak size katılıyorum, ancak bu farklılığı tespit etmenin oldukça zor olduğunu ve GC'nin sizin durumunuzda daha iyi çalıştığı sonucuna vardığınızda, bu tür bir fark önemliyse çok benzersiz bir durum olması gerektiğini unutmayın. Tersi ne olursa olsun, muhtemelen Nesne havuzu uygulamanızı kurtarabilir.
Ilya Gazman

1
Sadece argümanınızı anlamıyorum? GC'nin nesne havuzundan daha hızlı olup olmadığını tespit etmek çok mu zor? Ve bu nedenle nesne havuzunu kullanmalısınız? JVM, temiz kodlama ve kısa ömürlü nesneler için optimize edilmiştir. Eğer bu soru bunlarla ilgili ise (ki umarım OP bunlardan saniyede bir milyon üretirse) o zaman önerdiğiniz gibi daha karmaşık ve hataya açık bir şemaya geçmenin kanıtlanabilir bir avantajı varsa bu olmalıdır. Eğer bunu kanıtlamak çok zorsa, neden zahmet edelim?
Thorbjørn Ravn Andersen

0

Nesne havuzları, yığın üzerinde nesne tahsisine göre muazzam (bazen 10x) iyileştirmeler sağlar. Ancak bağlantılı bir liste kullanan yukarıdaki uygulama hem saf hem de yanlıştır! Bağlantılı liste, çabayı geçersiz kılarak iç yapısını yönetmek için nesneler oluşturur. Bir dizi nesneyi kullanan bir Ringbuffer iyi çalışır. Örnekte (hareketleri yöneten bir satranç programı) Ringbuffer, hesaplanan tüm hareketlerin listesi için bir tutucu nesneye sarılmalıdır. Daha sonra yalnızca hareket tutucu nesne referansları aktarılır.

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.