Quelles sont les «meilleures pratiques» pour créer (et publier) 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 mouvement possible, et une recherche nominale peut facilement générer plus d'un million d'objets de mouvement par seconde. Le GC JVM a été capable de gérer la charge de mon système de développement, mais je suis intéressé à explorer des approches alternatives qui:
- Minimisez la surcharge du garbage collection, et
- réduisez l'empreinte mémoire de pointe pour les systèmes bas de gamme.
Une grande majorité des objets sont de très courte durée, mais environ 1% des mouvements générés sont persistants et renvoyés en tant que valeur persistante, de sorte que toute technique de mise en commun ou de mise en cache devrait permettre d'exclure des objets spécifiques de la réutilisation. .
Je ne m'attends pas à un exemple de code complet, mais j'apprécierais des suggestions pour plus de lecture / recherche, ou des exemples open source de nature similaire.
la source
Réponses:
Exécutez l'application avec un garbage collection détaillé:
Et il vous dira quand il se rassemblera. Il y aurait deux types de balayages, un balayage rapide et un balayage complet.
La flèche est avant et après la taille.
Tant qu'il ne s'agit que de GC et non de GC complet, vous êtes en sécurité à la maison. Le GC standard est un collecteur de copies dans la «jeune génération», donc les objets qui ne sont plus référencés sont simplement oubliés, ce qui est exactement ce que vous voudriez.
La lecture du réglage de la récupération de place de la machine virtuelle HotSpot de Java SE 6 est probablement utile.
la source
Depuis la version 6, le mode serveur de JVM utilise une technique d' analyse d'échappement . En l'utilisant, vous pouvez éviter GC tous ensemble.
la source
Eh bien, il y a plusieurs questions en une ici!
1 - Comment sont gérés les objets éphémères?
Comme indiqué précédemment, la JVM peut parfaitement gérer une énorme quantité d'objets de courte durée, car elle suit l' hypothèse de génération faible .
Notez que nous parlons d'objets qui ont atteint la mémoire principale (tas). Ce n'est pas toujours le cas. Un grand nombre d'objets que vous créez ne quittent même pas un registre CPU. Par exemple, considérez cette boucle for
Ne pensons pas au déroulement de boucle (une optimisation que la JVM effectue fortement sur votre code). Si
max
est égal àInteger.MAX_VALUE
, votre boucle peut prendre un certain temps à s'exécuter. Cependant, lai
variable n'échappera jamais au bloc de boucle. Par conséquent, la JVM placera cette variable dans un registre CPU, l'incrémentera régulièrement mais ne la renverra jamais à la mémoire principale.Ainsi, créer des 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 à Eden, donc le GC ne les remarquera même pas.
2 - Est-il utile de réduire les frais généraux du GC?
Comme d'habitude, cela 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 GC, alors, oui, réglez le GC, sinon cela ne vaut peut-être pas vraiment la peine.
Par exemple, si vous avez un jeune GC toutes les 100ms qui prend 10ms, vous passez 10% de votre temps dans le GC, et vous avez 10 collections par seconde (ce qui est énorme). Dans un tel cas, je ne passerais pas de temps dans le réglage GC, puisque ces 10 GC / s seraient toujours là.
3 - Une certaine expérience
J'ai eu un problème similaire sur une application qui créait une énorme quantité d'une classe donnée. Dans les logs GC, j'ai remarqué que le taux de création de l'application était d'environ 3 Go / s, ce qui est bien trop (allez ... 3 gigaoctets de données par seconde?!).
Le problème: trop de GC fréquents causés par la création d'un trop grand nombre d'objets
Dans mon cas, j'ai attaché un profileur de mémoire et j'ai remarqué qu'une classe représentait un pourcentage énorme de tous mes objets. J'ai recherché 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:
Retravailler l'algorithme pour ne pas renvoyer une paire de booléens mais à la place j'ai deux méthodes qui retournent chaque booléen séparément
Mettez en 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 mettre une usine avec un cache non thread-safe (je n'avais pas besoin de thread safety puisque je n'aurais finalement que 4 instances différentes).
Le taux d'allocation est descendu à 1 Go / s, tout comme la fréquence des jeunes GC (divisée par 3).
J'espère que cela pourra aider !
la source
Si vous n'avez que des objets de valeur (c'est-à-dire, aucune référence à d'autres objets) et que je veux dire vraiment des tonnes et des tonnes d'entre eux, vous pouvez utiliser directement
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 le problème du GC presque entièrement tant que vous n'alloueriez qu'une seule fois, c'est-à-dire un gros morceau, puis gérez vous-même les objets. Au lieu de références, vous auriez seulement un index (c'est-à-dire
int
) dans leByteBuffer
qui doit être transmis. Vous devrez peut-être également aligner la mémoire.La technique donnerait l'impression d'être utilisée
C and void*
, mais avec un peu d'emballage, elle est supportable. Un inconvénient des performances pourrait être la vérification des limites si le compilateur ne parvient pas à l'éliminer. Un avantage majeur est la localité si vous traitez les tuples 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 JVM meurt de façon triviale et le coût d'allocation n'est qu'une bosse de pointeur. Le coût d'allocation peut être un peu plus élevé si vous utilisez des
final
champs car ils nécessitent une clôture de mémoire sur certaines plates-formes (à savoir ARM / Power), mais sur x86, il est gratuit.la source
En supposant que GC soit un problème (comme d'autres le soulignent peut-être pas), vous implémenterez votre propre gestion de la mémoire pour votre cas particulier, c'est-à-dire une classe qui souffre d'un taux de désabonnement massif. Essayez la mise en commun d'objets, j'ai vu des cas où cela fonctionne assez bien. La mise en œuvre de pools d'objets est un chemin bien parcouru, donc pas besoin de revenir ici, recherchez:
Mesurer avant / après etc., etc.
la source
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 des points en cache et les avons référencés au lieu de créer de nouvelles instances. Idem pour, par exemple, les chaînes vides.
Une autre source était plusieurs booléens qui ont été remplacés par un int et pour chaque booléen, nous utilisons un seul octet de l'int.
la source
J'ai traité ce scénario avec du code de traitement XML il y a quelque temps. Je me suis retrouvé à créer des millions d'objets de balises XML qui étaient très petits (généralement juste une chaîne) et extrêmement de courte durée (l'échec d'une vérification XPath signifiait pas de correspondance, donc jetez-les).
J'ai fait de sérieux tests et suis arrivé à la conclusion que je ne pouvais obtenir qu'une amélioration d'environ 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ée, j'ai trouvé que la file d'attente gratuite avait besoin d'un mécanisme ajouté pour l'élaguer si elle devenait trop grande - cela annulait complètement mon optimisation, je l'ai donc basculée sur une option.
En résumé - cela ne vaut probablement pas la peine - mais je suis heureux de voir que vous y pensez, cela montre que vous vous souciez.
la source
Étant donné que vous écrivez un programme d'échecs, il existe des techniques spéciales que vous pouvez utiliser pour 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 pousse quelques nombres sur la pile, par exemple, passez d'une case à l'autre. Au fur et à mesure que vous évaluerez l'arborescence de recherche, vous ferez apparaître des mouvements et mettre à jour une représentation du tableau.
Si vous voulez un pouvoir expressif, utilisez des objets. Si vous voulez de la vitesse (dans ce cas), passez au natif.
la source
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 mouvement, puis à annuler le mouvement avant de quitter la portée. Vous analysez probablement un seul mouvement à la fois, puis stockez simplement le meilleur mouvement quelque part.
Si ce n'est pas faisable pour une raison quelconque et que vous souhaitez réduire l'utilisation maximale de la mémoire, un bon article sur l'efficacité de la mémoire est ici: http://www.cs.virginia.edu/kim/publicity/pldi09tutorials/memory-effic-java- tutoriel.pdf
la source
Créez simplement vos millions d'objets et écrivez votre code de la bonne manière: ne gardez pas de références inutiles à ces objets. GC fera le sale boulot à votre place. Vous pouvez jouer avec les GC détaillés comme mentionné pour voir s'ils sont vraiment GC. Java IS sur la création et la libération d'objets. :)
la source
Je pense que vous devriez lire sur l'allocation de pile en Java et l'analyse d'échappement.
Parce que si vous approfondissez cette rubrique, 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 y a une explication wikipedia de l'analyse d'échappement, avec un exemple de son fonctionnement en Java:
http://en.wikipedia.org/wiki/Escape_analysis
la source
Je ne suis pas un grand fan de GC, donc j'essaie toujours de trouver des solutions. Dans ce cas, je suggérerais d'utiliser le modèle de pool d'objets :
L'idée est d'éviter de créer de nouveaux objets en les stockant dans une pile afin de pouvoir les réutiliser plus tard.
la source
Les pools d'objets fournissent d'énormes améliorations (parfois 10x) 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 chaînée crée des objets pour gérer sa structure interne annulant l'effort. Un Ringbuffer utilisant un tableau d'objets fonctionne bien. Dans l'exemple give (un programme d'échecs gérant les coups), le Ringbuffer doit être enveloppé dans un objet détenteur pour la liste de tous les coups calculés. Seules les références d'objet détenteur de mouvements seraient alors transmises.
la source