Quelles sont les "meilleures pratiques" pour créer (et libérer) des millions de petits objets?
J'écris un programme d'échecs en Java et l'algorithme de recherche génère un seul objet "Move" pour chaque coup possible, et une recherche nominale peut facilement générer plus d'un million d'objets par seconde. La JVM GC a été en mesure de gérer la charge de mon système de développement, mais je souhaiterais explorer d'autres approches qui:
La grande majorité des objets ont une durée de vie très courte, mais environ 1% des déplacements générés sont persistants et renvoyés sous forme de valeur persistante. Toute technique de mise en pool ou de mise en cache doit donc permettre d'exclure certains objets de la réutilisation. .
Je ne m'attends pas à un code d'exemple complet, mais j'apprécierais des suggestions de lectures/recherches supplémentaires ou des exemples à code source ouvert de nature similaire.
Exécutez l'application avec une récupération de place commentée:
Java -verbose:gc
Et il vous dira quand il se rassemblera. Il y aurait deux types de balayages, un rapide et un complet.
[GC 325407K->83000K(776768K), 0.2300771 secs]
[GC 325816K->83372K(776768K), 0.2454258 secs]
[Full GC 267628K->83769K(776768K), 1.8479984 secs]
La flèche est avant et après la taille.
Tant que vous ne faites que du GC et non du GC complet, vous êtes en sécurité chez vous. Le GC habituel est un collecteur de copies de la «jeune génération». Ainsi, les objets qui ne sont plus référencés sont tout simplement oubliés, ce qui correspond exactement à ce que vous souhaitez.
Lire Le réglage de la récupération de place dans la machine virtuelle JavaSpot 6 HotSpot est probablement utile.
Depuis la version 6, le mode serveur de la machine virtuelle Java utilise une technique escape analysis . En l'utilisant, vous pouvez éviter le GC tous ensemble.
Eh bien, il y a plusieurs questions en une ici!
1 - Comment les objets éphémères sont-ils gérés?
Comme indiqué précédemment, la machine virtuelle Java peut parfaitement gérer une énorme quantité d’objets de courte durée, car elle suit la hypothèse générationnelle faible .
Notez que nous parlons d'objets qui ont atteint la mémoire principale (tas). Ce n'est pas toujours le cas. Beaucoup d'objets que vous créez ne laissent même pas un registre de la CPU. Par exemple, considérons ceci pour la boucle
for(int i=0, i<max, i++) {
// stuff that implies i
}
Ne pensons pas au déroulement de la boucle (optimisations que la JVM effectue lourdement sur votre code). Si max
est égal à Integer.MAX_VALUE
, l'exécution de votre boucle peut prendre un certain temps. Cependant, la variable i
n'échappera jamais au bloc-boucle. Par conséquent, la machine virtuelle Java mettra cette variable dans un registre de la CPU, l'incrémentera régulièrement mais ne la renverra jamais dans la mémoire principale.
Ainsi, la création de millions d'objets n'est pas un problème s'ils ne sont utilisés que localement. Ils seront morts avant d'être stockés dans Eden, pour que le GC ne les remarque même pas.
2 - Est-il utile de réduire les frais généraux du GC?
Comme d'habitude, ça dépend.
Tout d'abord, vous devez activer la journalisation GC pour avoir une vue claire de ce qui se passe. Vous pouvez l'activer avec -Xloggc:gc.log -XX:+PrintGCDetails
.
Si votre application passe beaucoup de temps dans un cycle de GC, alors oui, réglez le GC, sinon, cela ne vaut peut-être pas la peine.
Par exemple, si vous avez un jeune GC toutes les 100 ms qui prend 10 ms, vous passez 10% de votre temps dans le GC et vous disposez de 10 collections par seconde (ce qui est énorme). Dans un tel cas, je ne passerais pas de temps à régler le GC, car ces 10 GC/s seraient toujours là.
3 - Un peu d'expérience
J'ai eu un problème similaire sur une application qui créait une quantité énorme d'une classe donnée. Dans les journaux du GC, j’ai remarqué que le taux de création de l’application était d’environ 3 Go/s, ce qui est beaucoup trop (allez ... 3 Go de données par seconde?!).
Le problème: trop de GC fréquents causés par trop d'objets créés.
Dans mon cas, j'ai attaché un profileur de mémoire et remarqué qu'une classe représentait un pourcentage énorme de tous mes objets. J'ai suivi les instanciations pour découvrir que cette classe était essentiellement une paire de booléens enveloppés dans un objet. Dans ce cas, deux solutions étaient disponibles:
Retravaillez l'algorithme pour que je ne retourne pas une paire de booléens mais deux méthodes qui renvoient chaque booléen séparément
Cache les objets, sachant qu'il n'y avait que 4 instances différentes
J'ai choisi le second, car il avait le moins d'impact sur l'application et était facile à introduire. Il m'a fallu quelques minutes pour installer une usine avec un cache non thread-safe (je n'avais pas besoin de la sécurité des threads car je n'aurais finalement que 4 instances différentes).
Le taux d'attribution est descendu à 1 Go/s, de même que la fréquence des jeunes GC (divisée par 3).
J'espère que cela pourra aider !
Si vous avez juste des objets de valeur (c'est-à-dire, aucune référence à d'autres objets) et vraiment mais je veux dire vraiment des tonnes et des tonnes d'entre eux, vous pouvez utiliser direct ByteBuffers
avec l'ordre des octets natif [ce dernier est important] et vous avez besoin de quelques centaines de lignes de code à allouer/réutiliser + getter/setters. Les Getters ressemblent à long getQuantity(int tupleIndex){return buffer.getLong(tupleInex+QUANTITY_OFFSSET);}
Cela résoudrait presque entièrement le problème du GC, à condition que vous allouiez une seule fois, c’est-à-dire un énorme bloc, puis que vous gériez les objets vous-même. Au lieu de références, vous auriez uniquement indexé (c'est-à-dire, int
) dans la ByteBuffer
à transmettre. Vous devrez peut-être faire en sorte que la mémoire s'aligne également.
La technique consisterait à utiliser C and void*
, mais avec certains emballages, elle est supportable. Un inconvénient des performances peut être la vérification des limites si le compilateur ne parvient pas à l'éliminer. Un avantage majeur est la localité si vous traitez les n-uplets comme des vecteurs, l'absence d'en-tête d'objet réduit également l'empreinte mémoire.
En dehors de cela, il est probable que vous n’auriez pas besoin d’une telle approche, car la jeune génération de pratiquement toutes les machines virtuelles meurt de manière triviale et le coût d’allocation n’est qu’une simple hausse. Le coût d'allocation peut être un peu plus élevé si vous utilisez des champs final
car ils nécessitent une barrière de mémoire sur certaines plates-formes (notamment ARM/Power), mais sur x86, il est gratuit.
En supposant que vous trouviez GC comme un problème (comme d’autres le soulignent, il se peut que vous ne mettiez pas en œuvre votre propre gestion de la mémoire pour votre cas particulier, c’est-à-dire une classe qui subit un déséquilibre important. Essayez de regrouper des objets, j'ai vu des cas où cela fonctionne assez bien. L'implémentation de pools d'objets est un chemin emprunté. Inutile de revenir ici, surveillez les points suivants:
Mesurer avant/après etc, etc
J'ai rencontré un problème similaire. Tout d’abord, essayez de réduire la taille des petits objets. Nous avons introduit des valeurs de champ par défaut les référençant dans chaque instance d'objet.
Par exemple, MouseEvent a une référence à la classe Point. Nous avons mis en cache des points et les avons référencés au lieu de créer de nouvelles instances. De même pour, par exemple, les chaînes vides.
Une autre source était plusieurs booléens qui ont été remplacés par un entier. Pour chaque booléen, nous utilisons un octet de ce entier.
J'ai traité ce scénario avec un code de traitement XML il y a quelque temps. Je me suis retrouvé à créer des millions d’objets de balise XML très petits (généralement une simple chaîne) et extrêmement éphémères (échec de XPath check signifiait qu’il n’y avait pas de correspondance).
J'ai fait des tests sérieux et je suis parvenu à la conclusion que je ne pouvais obtenir qu'une amélioration de 7% de la vitesse en utilisant une liste de balises supprimées au lieu d'en créer de nouvelles. Cependant, une fois implémenté, j'ai trouvé que la file d'attente libre avait besoin d'un mécanisme ajouté pour l'élaguer si elle devenait trop grosse - cela annulait complètement mon optimisation, je l'ai donc basculée sur une option.
En résumé - ça ne vaut probablement pas la peine - mais je suis content de voir que vous y réfléchissez, cela montre que vous vous en souciez.
Étant donné que vous écrivez un programme d’échecs, vous pouvez utiliser certaines techniques spéciales pour obtenir des performances décentes. Une approche simple consiste à créer un grand tableau de longs (ou octets) et à le traiter comme une pile. Chaque fois que votre générateur de mouvements crée des mouvements, il place plusieurs nombres dans la pile, par exemple. se déplacer de place et se déplacer à la place. Au fur et à mesure que vous évaluez l’arborescence de recherche, vous en sortez des mouvements et mettez à jour une représentation du tableau.
Si vous voulez des objets à usage expressif. Si vous voulez de la vitesse (dans ce cas), allez en natif.
Une solution que j'ai utilisée pour de tels algorithmes de recherche consiste à créer un seul objet Move, à le muter avec un nouveau déplacement, puis à annuler le déplacement avant de quitter l'étendue. Vous analysez probablement un seul mouvement à la fois, puis stockez le meilleur mouvement quelque part.
Si, pour une raison quelconque, cela n’est pas faisable et que vous souhaitez réduire l’utilisation maximale de la mémoire, voici un bon article sur l’efficacité de la mémoire: http://www.cs.virginia.edu/kim/publicity/pldi09tutorials/memory- efficient-Java-tutorial.pdf
Les pools d'objets apportent des améliorations considérables (parfois 10 fois) par rapport à l'allocation d'objets sur le tas. Mais l'implémentation ci-dessus utilisant une liste chaînée est à la fois naïve et fausse! La liste liée crée des objets pour gérer sa structure interne, ce qui annule l'effort .. Un tampon mémoire utilisant un tableau d'objets fonctionne bien. Dans l'exemple donne (un programme d'échecs gérant des mouvements), le tampon de sonnerie doit être encapsulé dans un objet titulaire pour la liste de tous les mouvements calculés. Seules les références d'objet détenteur de mouvements seraient alors transmises.
Créez simplement vos millions d'objets et écrivez votre code correctement: ne gardez pas de références inutiles à ces objets. GC fera le sale boulot pour vous. Vous pouvez jouer avec les GC verbales, comme mentionné, pour voir si elles sont vraiment GC. Java IS sur la création et la libération d'objets. :)
Je pense que vous devriez lire à propos de l'allocation de pile en Java et de l'analyse d'évasion.
Si vous approfondissez ce sujet, vous constaterez peut-être que vos objets ne sont même pas alloués sur le tas et qu'ils ne sont pas collectés par GC de la même manière que les objets sur le tas.
Il existe une explication wikipedia de l'analyse d'évasion, avec un exemple de son fonctionnement en Java:
Je ne suis pas un grand fan de GC, alors j'essaie toujours de trouver un moyen de le contourner. Dans ce cas, je suggérerais d'utiliser modèle de pool d'objets :
L'idée est d'éviter de créer de nouveaux objets en les stockant dans une pile pour pouvoir les réutiliser ultérieurement.
Class MyPool
{
LinkedList<Objects> stack;
Object getObject(); // takes from stack, if it's empty creates new one
Object returnObject(); // adds to stack
}