Beste werkwijze voor het maken van miljoenen kleine tijdelijke objecten

Wat zijn de “best practices” voor het maken (en vrijgeven) van miljoenen kleine objecten?

Ik ben een schaakprogramma aan het schrijven in Java en het zoekalgoritme genereert een enkel “Verplaats”-object voor elke mogelijke zet, en een nominale zoekopdracht kan gemakkelijk meer dan een miljoen verplaatsingsobjecten per seconde genereren. De JVM GC heeft de belasting van mijn ontwikkelsysteem aankunnen, maar ik ben geïnteresseerd in het verkennen van alternatieve benaderingen die:

  1. Minimaliseer de overhead van het ophalen van afval, en
  2. verminder de piekgeheugenvoetafdruk voor lagere systemen.

Een overgrote meerderheid van de objecten heeft een zeer korte levensduur, maar ongeveer 1% van de gegenereerde bewegingen wordt behouden en geretourneerd als de persistente waarde, dus elke pooling- of cachingtechniek zou de mogelijkheid moeten bieden om specifieke objecten uit te sluiten van hergebruikt.

Ik verwacht geen volledig uitgewerkte voorbeeldcode, maar ik zou graag suggesties voor verder lezen/onderzoek of open source-voorbeelden van vergelijkbare aard waarderen.


Antwoord 1, autoriteit 100%

Voer de applicatie uit met uitgebreide garbagecollection:

java -verbose:gc

En het zal u vertellen wanneer het wordt verzameld. Er zijn twee soorten sweeps, een snelle en een volledige sweep.

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

De pijl staat voor en na maat.

Zolang het alleen GC doet en geen volledige GC, ben je veilig thuis. De reguliere GC is een kopieerverzamelaar in de ‘jonge generatie’, dus objecten waarnaar niet meer wordt verwezen, worden gewoon vergeten, en dat is precies wat je zou willen.

Lezen Java SE 6 HotSpot Virtual Machine Garbage Collection Tuningis waarschijnlijk nuttig.


Antwoord 2, autoriteit 45%

Sinds versie 6 maakt de servermodus van JVM gebruik van een ontsnappingsanalysetechniek. Als je het gebruikt, kun je GC helemaal vermijden.


Antwoord 3, autoriteit 40%

Nou, er zijn hier meerdere vragen in één!

1 – Hoe worden objecten met een korte levensduur beheerd?

Zoals eerder vermeld, kan de JVM perfect omgaan met een enorme hoeveelheid kortlevende objecten, omdat het de Zwakke generatiehypothese.

Merk op dat we het hebben over objecten die het hoofdgeheugen (heap) hebben bereikt. Dit is niet altijd het geval. Veel objecten die u maakt, verlaten niet eens een CPU-register. Overweeg bijvoorbeeld deze for-loop

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

Laten we niet denken aan het uitrollen van een lus (een optimalisatie die de JVM zwaar op uw code uitvoert). Als maxgelijk is aan Integer.MAX_VALUE, kan het enige tijd duren voordat de lus is uitgevoerd. De variabele izal echter nooit ontsnappen aan het loop-blok. Daarom zal de JVM die variabele in een CPU-register plaatsen, deze regelmatig verhogen, maar nooit terugsturen naar het hoofdgeheugen.

Het maken van miljoenen objecten is dus niet erg als ze alleen lokaal worden gebruikt. Ze zullen dood zijn voordat ze in Eden worden opgeslagen, dus de WG zal ze niet eens opmerken.

2 – Is het nuttig om de overhead van de GC te verminderen ?

Zoals gewoonlijk hangt het ervan af.

Eerst moet u GC-logboekregistratie inschakelen om een ​​duidelijk beeld te krijgen van wat er aan de hand is. U kunt het inschakelen met -Xloggc:gc.log -XX:+PrintGCDetails.

Als uw toepassing veel tijd in een GC-cyclus doorbrengt, moet u de GC afstemmen, anders is het misschien niet echt de moeite waard.

Als je bijvoorbeeld elke 100 ms een jonge GC hebt die 10 ms duurt, breng je 10% van je tijd in de GC door en heb je 10 verzamelingen per seconde (wat enorm is). In zo’n geval zou ik geen tijd besteden aan GC-tuning, aangezien die 10 GC/s er nog steeds zouden zijn.

3 – Enige ervaring

Ik had een soortgelijk probleem met een applicatie die een enorme hoeveelheid van een bepaalde klasse aanmaakte. In de GC-logboeken zag ik dat de aanmaaksnelheid van de applicatie ongeveer 3 GB/s was, wat veel te veel is (kom op… 3 gigabyte aan gegevens per seconde?!).

Het probleem: te veel frequente GC veroorzaakt door te veel objecten die worden gemaakt.

In mijn geval heb ik een geheugenprofiler toegevoegd en merkte dat een klasse een enorm percentage van al mijn objecten vertegenwoordigde. Ik heb de instantiaties opgespoord om erachter te komen dat deze klasse in feite een paar booleans was verpakt in een object. In dat geval waren er twee oplossingen beschikbaar:

  • Bewerk het algoritme zodat ik geen paar booleans retourneer, maar in plaats daarvan heb ik twee methoden die elke boolean afzonderlijk retourneren

  • Cache de objecten, wetende dat er maar 4 verschillende instanties waren

Ik koos voor de tweede, omdat deze de minste impact had op de applicatie en gemakkelijk te introduceren was. Het kostte me minuten om een ​​fabriek met een niet-thread-safe cache te plaatsen (ik had geen thread-beveiliging nodig omdat ik uiteindelijk maar 4 verschillende instanties zou hebben).

De toewijzingssnelheid daalde tot 1 GB/s, en dat gold ook voor de frequentie van jonge GC (gedeeld door 3).

Hopelijk helpt dat!


Antwoord 4, autoriteit 23%

Als je alleen waarde-objecten hebt (dat wil zeggen, geen verwijzingen naar andere objecten) en echt, maar ik bedoel echt heel veel en tonnen, dan kun je directe ByteBuffersgebruiken met native byte-volgorde [de laatste is belangrijk] en je hebt een paar honderd regels code nodig om + getter/setters toe te wijzen/hergebruiken. Getters lijken op long getQuantity(int tupleIndex){return buffer.getLong(tupleInex+QUANTITY_OFFSSET);}

Dat zou het GC-probleem bijna volledig oplossen, zolang je maar één keer toewijst, dat wil zeggen een enorm stuk, en dan de objecten zelf beheert. In plaats van referenties zou je alleen index (dat wil zeggen, int) hebben in de ByteBufferdie moet worden doorgegeven. Mogelijk moet u het geheugen ook zelf uitlijnen.

De techniek zou aanvoelen als het gebruik van C and void*, maar met wat verpakking is het draaglijk. Een nadeel van de prestaties kan zijn dat er grenzen worden gecontroleerd als de compiler deze niet kan elimineren. Een groot voordeel is de lokaliteit als je de tuples als vectoren verwerkt, het ontbreken van de objectheader vermindert ook de geheugenvoetafdruk.

Anders dan dat, is het waarschijnlijk dat je zo’n aanpak niet nodig hebt, aangezien de jonge generatie van vrijwel alle JVM’s triviaal sterft en de toewijzingskosten slechts een indicatie zijn. De toewijzingskosten kunnen iets hoger zijn als u final-velden gebruikt, omdat ze op sommige platforms geheugenafrastering vereisen (namelijk ARM/Power), op x86 is het echter gratis.


Antwoord 5, autoriteit 17%

Ervan uitgaande dat u vindt dat GC een probleem is (zoals anderen aangeven dat het misschien niet zo is), implementeert u uw eigen geheugenbeheer voor uw speciale geval, d.w.z. een klasse die te lijden heeft onder enorme churn. Probeer het poolen van objecten eens, ik heb gevallen gezien waarin het best goed werkt. Het implementeren van objectpools is een betreden pad, dus u hoeft hier niet opnieuw te bezoeken, let op:

  • multi-threading: het gebruik van lokale threadpools kan in uw geval werken
  • ondersteunende gegevensstructuur: overweeg het gebruik van ArrayDeque omdat het goed presteert bij verwijderen en geen toewijzingsoverhead heeft
  • beperk de grootte van je zwembad 🙂

Meet voor/na etc, etc


Antwoord 6, autoriteit 13%

Ik ben een soortgelijk probleem tegengekomen. Probeer allereerst de grootte van de kleine objecten te verkleinen. We hebben enkele standaard veldwaarden geïntroduceerd die ernaar verwijzen in elke objectinstantie.

MouseEvent heeft bijvoorbeeld een verwijzing naar de klasse Point. We hebben Points in de cache opgeslagen en ernaar verwezen in plaats van nieuwe instanties te maken. Hetzelfde geldt voor bijvoorbeeld lege strings.

Een andere bron waren meerdere booleans die werden vervangen door één int en voor elke boolean gebruiken we slechts één byte van de int.


Antwoord 7, autoriteit 13%

Ik heb dit scenario enige tijd geleden behandeld met een XML-verwerkingscode. Ik merkte dat ik miljoenen XML-tagobjecten maakte die erg klein waren (meestal slechts een string) en extreem kortstondig (falen van een XPathvinkje betekende geen match, dus weggooien).

Ik heb een aantal serieuze tests gedaan en kwam tot de conclusie dat ik slechts ongeveer 7% snelheidsverbetering kon bereiken door een lijst met afgedankte tags te gebruiken in plaats van nieuwe te maken. Eenmaal geïmplementeerd ontdekte ik echter dat de gratis wachtrij een mechanisme nodig had om het te snoeien als het te groot werd – dit maakte mijn optimalisatie volledig teniet, dus schakelde ik het over naar een optie.

Samengevat – waarschijnlijk niet de moeite waard – maar ik ben blij om te zien dat je erover nadenkt, het laat zien dat je erom geeft.


Antwoord 8, autoriteit 4%

Aangezien u een schaakprogramma schrijft, zijn er enkele speciale technieken die u kunt gebruiken voor goede prestaties. Een eenvoudige benadering is om een ​​grote reeks longs (of bytes) te maken en deze als een stapel te behandelen. Elke keer dat je zetgenerator zetten maakt, duwt hij een aantal getallen op de stapel, b.v. ga van vierkant en ga naar vierkant. Terwijl u de zoekboom evalueert, maakt u zetten en werkt u een bordweergave bij.

Als je expressieve kracht wilt, gebruik dan objecten. Als je snelheid wilt (in dit geval), ga dan voor native.


Antwoord 9, autoriteit 2%

Een oplossing die ik voor dergelijke zoekalgoritmen heb gebruikt, is om slechts één Move-object te maken, het te muteren met een nieuwe zet en de verplaatsing vervolgens ongedaan te maken voordat u het bereik verlaat. U analyseert waarschijnlijk slechts één zet tegelijk en slaat vervolgens de beste zet ergens op.

Als dat om de een of andere reden niet haalbaar is en u het piekgeheugengebruik wilt verminderen, vindt u hier een goed artikel over geheugenefficiëntie: http://www.cs.virginia.edu/kim/publicity/pldi09tutorials/memory-efficient-java-tutorial.pdf


Antwoord 10

Maak uw miljoenen objecten en schrijf uw code op de juiste manier: bewaar geen onnodige verwijzingen naar deze objecten. GC zal het vuile werk voor u doen. Je kunt spelen met uitgebreide GC zoals vermeld om te zien of ze echt GC’d zijn. Java IS over het maken en vrijgeven van objecten. 🙂


Antwoord 11

Ik denk dat je moet lezen over stapeltoewijzing in Java en ontsnappingsanalyse.

Want als je dieper op dit onderwerp ingaat, zul je ontdekken dat je objecten niet eens op de heap zijn toegewezen en dat ze niet door GC worden verzameld zoals objecten op de heap.

Er is een wikipedia-uitleg van ontsnappingsanalyse, met een voorbeeld van hoe dit werkt in Java:

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


Antwoord 12

Ik ben geen grote fan van GC, dus ik probeer altijd manieren te vinden om het te omzeilen. In dit geval raad ik aan om Object Pool-patroonte gebruiken:

Het idee is om te voorkomen dat er nieuwe objecten worden gemaakt door ze in een stapel op te slaan, zodat je ze later opnieuw kunt gebruiken.

Class MyPool
{
   LinkedList<Objects> stack;
   Object getObject(); // takes from stack, if it's empty creates new one
   Object returnObject(); // adds to stack
}

Antwoord 13

Objectpools bieden enorme (soms 10x) verbeteringen ten opzichte van objecttoewijzing op de heap. Maar de bovenstaande implementatie met behulp van een gekoppelde lijst is zowel naïef als verkeerd! De gekoppelde lijst maakt objecten om de interne structuur te beheren, waardoor de inspanning teniet wordt gedaan.
Een Ringbuffer die een reeks objecten gebruikt, werkt goed. In het voorbeeld give (een schaakprogramma dat zetten beheert) moet de Ringbuffer worden verpakt in een houderobject voor de lijst van alle berekende zetten. Alleen de objectreferenties van de verplaatsingshouder zouden dan worden doorgegeven.

Other episodes