Meilleures pratiques pour créer des millions de petits objets temporaires


109

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:

  1. Minimisez la surcharge du garbage collection, et
  2. 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.


11
Le modèle Flyweight serait-il approprié pour votre cas? en.wikipedia.org/wiki/Flyweight_pattern
Roger Rowland

4
Avez-vous besoin de l'encapsuler dans un objet?
nhahtdh

1
Le modèle Flyweight n'est pas approprié, car les objets ne partagent pas de données communes importantes. Quant à l'encapsulation des données dans un objet, il est trop volumineux pour être emballé dans une primitive, c'est pourquoi je recherche des alternatives aux POJO.
Humble Programmer

2
Lecture hautement recommandée: cs.virginia.edu/kim/publicity/pldi09tutorials/…
rkj

Réponses:


47

Exécutez l'application avec un garbage collection détaillé:

java -verbose:gc

Et il vous dira quand il se rassemblera. Il y aurait deux types de balayages, un balayage rapide et un balayage 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 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.


Expérimentez avec la taille du tas Java pour essayer de trouver un point où le garbage collection complet est rare. Dans Java 7, le nouveau G1 GC est plus rapide dans certains cas (et plus lent dans d'autres).
Michael Shops

21

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.


1
L'analyse d'évasion déçoit souvent, il vaut la peine de vérifier si la JVM a compris ce que vous faites ou non.
Nitsan Wakart

2
Si vous avez de l'expérience avec ces options: -XX: + PrintEscapeAnalysis et -XX: + PrintEliminateAllocations. Ce serait formidable de partager. Parce que je ne le dis pas, honnêtement.
Mikhail

voir stackoverflow.com/questions/9032519/ ... vous devrez obtenir une version de débogage pour JDK 7, j'avoue que je ne l'ai pas fait mais avec JDK 6, cela a réussi.
Nitsan Wakart

19

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

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

Ne pensons pas au déroulement de boucle (une optimisation que la JVM effectue fortement sur votre code). Si maxest égal à Integer.MAX_VALUE, votre boucle peut prendre un certain temps à s'exécuter. Cependant, la ivariable 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 !


11

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 ByteBuffersavec 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 le ByteBufferqui 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 finalchamps car ils nécessitent une clôture de mémoire sur certaines plates-formes (à savoir ARM / Power), mais sur x86, il est gratuit.


8

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:

  • multi-threading: l'utilisation de pools de threads locaux peut fonctionner pour votre cas
  • structure de données de sauvegarde: envisagez d'utiliser ArrayDeque car il fonctionne bien lors de la suppression et n'a pas de surcharge d'allocation
  • limitez la taille de votre piscine :)

Mesurer avant / après etc., etc.


6

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.


Juste par intérêt: qu'est-ce que cela vous a apporté en termes de performances? Avez-vous profilé votre candidature avant et après le changement, et si oui, quels en ont été les résultats?
Axel

@Axel les objets utilisent beaucoup moins de mémoire, donc GC n'est pas appelé aussi souvent. Nous avons certainement profilé notre application, mais il y avait même un effet visuel de la vitesse améliorée.
StanislavL

6

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.


2

É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.


1

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


Lien mort. Y a-t-il une autre source pour cet article?
dnault

0

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. :)


1
Désolé mon pote, je ne suis pas d'accord avec votre approche ... Java, comme tout langage de programmation, consiste à résoudre un problème dans ses contraintes, si l'OP est contraint par GC, comment l'aides-tu?
Nitsan Wakart

1
Je lui explique comment Java fonctionne réellement. S'il est incapable d'esquiver la situation d'avoir des millions d'objets temporaires, le meilleur conseil pourrait être, la classe temporaire doit être légère et il doit s'assurer qu'il libère les références le plus tôt possible, pas une seule étape. Est-ce que je manque quelque chose?
gyorgyabraham

Java prend en charge la création de déchets et les nettoie pour vous, c'est vrai. Si l'OP ne peut pas esquiver la création d'objets et qu'il n'est pas satisfait du temps passé en GC, c'est une triste fin. Mon objection concerne la recommandation que vous faites de faire plus de travail pour GC parce que c'est en quelque sorte Java approprié.
Nitsan Wakart

0

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


0

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.

Class MyPool
{
   LinkedList<Objects> stack;

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

3
Utiliser un pool pour de petits objets est une assez mauvaise idée, vous avez besoin d'un pool par thread pour démarrer (ou l'accès partagé tue toute performance). Ces pools fonctionnent également moins bien qu'un bon garbage collector. Dernier: le GC est une aubaine lorsqu'il s'agit de traiter avec du code / des structures simultanés - de nombreux algorithmes sont beaucoup plus faciles à implémenter car il n'y a naturellement pas de problème ABA. Réf. le comptage dans un environnement concurrent nécessite au moins une opération atomique + une clôture de mémoire (LOCK ADD ou CAS sur x86)
bestsss

1
La gestion des objets dans le pool peut être plus coûteuse que de laisser s'exécuter le ramasse-miettes.
Thorbjørn Ravn Andersen

@ ThorbjørnRavnAndersen En général, je suis d'accord avec vous, mais notez que détecter une telle différence est tout un défi, et lorsque vous parvenez à la conclusion que GC fonctionne mieux dans votre cas, ce doit être un cas tout à fait unique si une telle différence compte. Dans tous les cas, il se peut que le pool d'objets enregistre votre application.
Ilya Gazman

1
Je ne comprends tout simplement pas votre argument? Il est très difficile de détecter si GC est plus rapide que le pool d'objets? Et par conséquent, vous devriez utiliser le pool d'objets? La JVM est optimisée pour un codage propre et des objets de courte durée. Si tel est le sujet de cette question (et j'espère que si OP en génère un million par seconde), ce ne devrait être que s'il y a un avantage prouvable à passer à un schéma plus complexe et sujet aux erreurs que celui que vous suggérez. Si cela est trop difficile à prouver, alors pourquoi s'embêter.
Thorbjørn Ravn Andersen

0

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.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.