109 votes

Meilleure pratique pour créer des millions de petits objets temporaires

Quelles sont les "meilleures pratiques" pour créer (et diffuser) 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 de mouvement par seconde. Le GC de la JVM a été capable de gérer la charge sur mon système de développement, mais je suis intéressé par l'exploration d'approches alternatives qui le feraient :

  1. minimiser les frais de collecte des déchets et
  2. réduire l'empreinte mémoire maximale pour les systèmes bas de gamme.

La grande majorité des objets ont une durée de vie très courte, mais environ 1 % des mouvements générés sont persistés et renvoyés en tant que valeur persistée, 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 code d'exemple complet, mais j'apprécierais des suggestions de lectures/recherches supplémentaires, ou des exemples open source de nature similaire.

84voto

Marko Topolnik Points 77257

Le GC de la JVM est optimisé de manière agressive pour les grandes quantités d'objets à courte durée de vie. Ils sont à la fois incroyablement bon marché à allouer et à collecter dans leur ensemble - surtout si vous ne les mélangez pas avec l'allocation d'objets à long ou moyen terme. La quantité de tas régule la fréquence de ces passages mineurs de la GC, mais même avec un tas limité, vous n'aurez peut-être pas de gros problèmes avec cela.

Il faut absolument faire des tests avant de remanier l'algorithme pour minimiser les déchets.

47voto

Niels Bech Nielsen Points 2288

Exécutez l'application avec une collecte de déchets verbale :

java -verbose:gc

Et il vous dira quand il sera collecté. Il y aurait deux types de balayage, 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 représente la taille avant et après.

Tant qu'il ne s'agit que d'une GC et non d'une GC complète, vous êtes en sécurité. La GC normale 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 voulez.

Lecture Optimisation de la collecte d'ordures de la machine virtuelle HotSpot de Java SE 6 est probablement utile.

21voto

Mikhail Points 2585

Depuis la version 6, le mode serveur de la JVM utilise un système de contrôle de l'accès. analyse de l'évasion technique. En l'utilisant, vous pouvez éviter le GC.

18voto

Pierre Laporte Points 732

Eh bien, il y a plusieurs questions en une seule ici !

1 - Comment sont gérés les objets éphémères ?

Comme indiqué précédemment, la JVM peut parfaitement gérer une grande quantité d'objets de courte durée, puisqu'elle suit le principe de l'utilisation des objets de courte durée. Hypothèse générationnelle faible .

Notez que nous parlons d'objets qui ont atteint la mémoire principale (heap). Ce n'est pas toujours le cas. Beaucoup d'objets que vous créez ne quittent même pas un registre du CPU. Par exemple, considérez ce for-loop

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

Ne pensons pas au déroulage des boucles (une optimisation que la JVM effectue lourdement sur votre code). Si max est égal à Integer.MAX_VALUE votre boucle peut prendre un certain temps à s'exécuter. Cependant, le i n'échappera jamais au bloc-boucle. Par conséquent, la JVM placera cette variable dans un registre du 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 gros problème s'ils ne sont utilisés que localement. Ils seront morts avant d'être stockés dans Eden, donc le GC ne les remarquera même pas.

2 - Est-il utile de réduire l'overhead du GC ?

Comme d'habitude, cela dépend.

Tout d'abord, vous devez activer la journalisation GC pour avoir une vision 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, accordez le GC, sinon, cela ne vaut pas vraiment la peine.

Par exemple, si vous avez une jeune GC toutes les 100ms qui prend 10ms, vous passez 10% de votre temps dans la GC, et vous avez 10 collections par seconde (ce qui est huuuu énorme). Dans un tel cas, je ne passerais pas de temps à régler le 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 quantité énorme d'une classe donnée. Dans les journaux 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 gigaoctets de données par seconde ? !).

Le problème : Trop de GC fréquents causés par la création de trop 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 s'offraient à moi :

  • Retravailler l'algorithme de manière à ne pas renvoyer une paire de booléens mais à avoir deux méthodes qui renvoient chaque booléen séparément.

  • Mettre en cache les objets, sachant qu'il n'y avait que 4 instances différentes

J'ai choisi la seconde, car elle avait le moins d'impact sur l'application et était facile à introduire. Il m'a fallu quelques minutes pour mettre en place une usine avec un cache non sécurisé par les threads (je n'avais pas besoin de la sécurité des threads puisque je n'aurais finalement que 4 instances différentes).

Le taux d'allocation est passé à 1 Go/s, de même que la fréquence des jeunes GC (divisée par 3).

J'espère que cela vous aidera !

11voto

bestsss Points 6403

Si vous n'avez que des objets de valeur (c'est-à-dire aucune référence à d'autres objets) et qu'il y en a vraiment, mais je veux dire vraiment des tonnes et des tonnes, vous pouvez utiliser la commande directe ByteBuffers avec un ordre d'octet natif [ce dernier est important] et vous avez besoin de quelques centaines de lignes de code pour allouer/réutiliser + getter/setters. Les getter ressemblent à long getQuantity(int tupleIndex){return buffer.getLong(tupleInex+QUANTITY_OFFSSET);}

Cela résoudrait presque entièrement le problème de la GC tant que vous n'allouez qu'une seule fois, c'est-à-dire un gros morceau, et que vous gérez ensuite les objets vous-même. Au lieu des références, vous n'auriez que des index (c'est-à-dire, int ) dans le ByteBuffer qui doit être transmis. Il se peut que vous deviez aussi faire l'alignement de la mémoire vous-même.

La technique donnerait l'impression d'utiliser C and void* mais avec un peu d'emballage, c'est supportable. Un inconvénient de performance pourrait être le contrôle 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'ayez pas besoin d'une telle approche car la jeune génération de pratiquement toutes les JVM meurt de manière 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 final champs car ils nécessitent une clôture mémoire sur certaines plateformes (à savoir ARM/Power), sur x86 c'est gratuit, cependant.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X