paint-brush
Le logiciel prospère à moins que vous ne le tuiez d’abord : optimisation prématurée et histoire de Java GCpar@wasteofserver
511 lectures
511 lectures

Le logiciel prospère à moins que vous ne le tuiez d’abord : optimisation prématurée et histoire de Java GC

par Frankie6m2024/04/06
Read on Terminal Reader

Trop long; Pour lire

N'allez pas trop loin avec les optimisations, laissez le langage travailler pour vous. Histoire raconter. Les mises à niveau Java améliorent gratuitement les performances. Une référence, toujours.
featured image - Le logiciel prospère à moins que vous ne le tuiez d’abord : optimisation prématurée et histoire de Java GC
Frankie HackerNoon profile picture
0-item

Une LinkedList sera-t-elle plus rapide ? Dois-je échanger le « for each » avec un « itérateur » ? Cette « ArrayList » devrait-elle être un « Tableau » ? Cet article est né en réponse à une optimisation si malveillante qu’elle est restée gravée pour toujours dans ma mémoire.


Avant d'aborder Java et les moyens de lutter contre les interférences, provenant soit du ramasse-miettes, soit du changement de contexte, jetons d'abord un coup d'œil sur les principes fondamentaux de l'écriture de code pour votre futur.


L'optimisation prématurée est la racine de tout Mal.


Vous l'avez déjà entendu ; L'optimisation prématurée est la racine de tout Mal. Eh bien, parfois. Lorsque j'écris des logiciels, je crois fermement qu'il faut être :


  1. aussi descriptif que possible ; vous devriez essayer de raconter vos intentions comme si vous écriviez une histoire.


  2. aussi optimal que possible ; ce qui signifie que vous devez connaître les bases de la langue et les appliquer en conséquence.

Aussi descriptif que possible

Votre code doit exprimer une intention, et cela dépend en grande partie de la façon dont vous nommez les méthodes et les variables.


 int[10] array1; // bad int[10] numItems; // better int[10] backPackItems; // great

Rien que par le nom de la variable, vous pouvez déjà déduire la fonctionnalité.


Bien que numItems soit abstrait, backPackItems vous en dit long sur le comportement attendu.


Ou disons que vous avez cette méthode :


 List<Countries> visitedCountries() { if(noCountryVisitedYet) return new ArrayList<>(0); } // (...) return listOfVisitedCountries; }

En ce qui concerne le code, cela semble plus ou moins correct.


Pouvons-nous faire mieux ? Nous pouvons certainement le faire !


 List<Countries> visitedCountries() { if(noCountryVisitedYet) return Collections.emptyList(); } // (...) return listOfVisitedCountries; }

La lecture Collections.emptyList() est beaucoup plus descriptive que new ArrayList<>(0);


Imaginez que vous lisez le code ci-dessus pour la première fois et que vous tombiez sur la clause de garde qui vérifie si l'utilisateur a réellement visité des pays. Imaginez également que cela soit enterré dans une longue classe, la lecture Collections.emptyList() est nettement plus descriptive que new ArrayList<>(0) , vous vous assurez également qu'il est immuable en vous assurant que le code client ne peut pas le modifier.

Aussi optimal que possible

Connaissez votre langue et utilisez-la en conséquence. Si vous avez besoin d'un double , il n'est pas nécessaire de l'envelopper dans un objet Double . Il en va de même pour l’utilisation d’un List si tout ce dont vous avez réellement besoin est un Array .


Sachez que vous devez concaténer des chaînes à l'aide StringBuilder ou StringBuffer si vous partagez l'état entre les threads :


 // don't do this String votesByCounty = ""; for (County county : counties) { votesByCounty += county.toString(); } // do this instead StringBuilder votesByCounty = new StringBuilder(); for (County county : counties) { votesByCounty.append(county.toString()); }


Sachez comment indexer votre base de données. Anticipez les goulots d’étranglement et mettez en cache en conséquence. Tout ce qui précède sont des optimisations. C’est le genre d’optimisations dont vous devez être conscient et mettre en œuvre en tant que premiers citoyens.

Comment le tuer en premier ?

Je n'oublierai jamais un hack que j'ai lu il y a quelques années. À vrai dire, l’auteur a rapidement fait marche arrière, mais cela montre à quel point de bonnes intentions peuvent susciter beaucoup de mal.


 // do not do this, ever! int i = 0; while (i<10000000) { // business logic if (i % 3000 == 0) { //prevent long gc try { Thread.sleep(0); } catch (Ignored e) { } } }

Un hack d'éboueur de l'enfer !


Vous pouvez en savoir plus sur pourquoi et comment le code ci-dessus fonctionne dans l'article original et, bien que l'exploit soit certainement intéressant, c'est l'une de ces choses que vous ne devriez jamais faire.


  • Fonctionne par effets secondaires, Thread.sleep(0) n'a aucun but dans ce bloc
  • Fonctionne en exploitant une déficience de code en aval
  • Pour quiconque hérite de ce code, c'est obscur et magique


Ne commencez à forger quelque chose d'un peu plus complexe que si, après avoir écrit avec toutes les optimisations par défaut fournies par le langage , vous rencontrez un goulot d'étranglement. Mais évitez les concoctions comme ci-dessus.


Une interprétation du futur Garbage Collector de Java "imaginé" par Microsoft Copilot


Comment s’attaquer à cet éboueur ?

Si après tout, le Garbage Collector est toujours la pièce qui offre de la résistance, voici quelques-unes des choses que vous pouvez essayer :


  • Si votre service est si sensible à la latence que vous ne pouvez pas autoriser le GC, exécutez avec "Epsilon GC" et évitez complètement le GC .
    -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC


    Cela augmentera évidemment votre mémoire jusqu'à ce que vous obteniez une exception MOO, donc soit c'est un scénario de courte durée, soit votre programme est optimisé pour ne pas créer d'objets.


  • Si votre service est quelque peu sensible à la latence, mais que la tolérance autorisée permet une certaine marge de manœuvre , exécutez GC1 et alimentez-le quelque chose comme -XX:MaxGCPauseTimeMillis=100 (la valeur par défaut est 250 ms)

  • Si le problème provient de bibliothèques externes , disons que l'une d'entre elles appelle System.gc() ou Runtime.getRuntime().gc() qui sont des éboueurs stop-the-world, vous pouvez remplacer le comportement incriminé en exécutant avec -XX:+DisableExplicitGC


  • Si vous utilisez une JVM supérieure à 11, essayez le Z Garbage Collector (ZGC) , les améliorations de performances sont monumentales ! -XX:+UnlockExperimentalVMOptions -XX:+UseZGC . Vous pouvez également consulter ce benchmark JDK 21 GC .


DÉBUT DE LA VERSION

FIN DE LA VERSION

CG PAR DÉFAUT

Java1

Java4

Collecteur de déchets en série

Java5

Java8

Collecteur de déchets parallèle

Java9

en cours

Collecteur de déchets G1


Remarque 1 : depuis Java 15, ZGC est prêt pour la production , mais vous devez toujours l'activer explicitement avec -XX:+UseZGC .


Remarque 2 : La VM considère les machines comme étant de classe serveur si la VM détecte plus de deux processeurs et une taille de segment supérieure ou égale à 1 792 Mo. S'il ne s'agit pas d'une classe serveur, il s'agira par défaut du Serial GC .


Essentiellement, optez pour le réglage GC lorsqu'il est clair que les contraintes de performances de l'application sont directement liées au comportement du garbage collection et que vous disposez de l'expertise nécessaire pour effectuer des ajustements éclairés. Sinon, faites confiance aux paramètres par défaut de la JVM et concentrez-vous sur l'optimisation du code au niveau de l'application.

u/shiphe - vous voudrez lire le commentaire complet


Autres bibliothèques pertinentes que vous voudrez peut-être explorer :

Harnais de microbenchmark Java (JMH)

Si vous optimisez par ressenti sans véritable analyse comparative, vous ne vous rendez pas service. JMH est la bibliothèque Java de facto pour tester les performances de vos algorithmes. Utilise le.

Java-Thread-Affinité

Épingler un processus sur un cœur spécifique peut améliorer les accès au cache. Cela dépendra du matériel sous-jacent et de la manière dont votre routine traite les données. Néanmoins, cette bibliothèque la rend si facile à implémenter que, si une méthode gourmande en CPU vous traîne, vous aurez envie de la tester.

Perturbateur LMAX

C'est une de ces bibliothèques que, même si vous n'en avez pas besoin, vous aurez envie d'étudier. L’idée est de permettre une concurrence à latence ultra faible. Mais la manière dont il est mis en œuvre, de la sympathie mécanique au tampon annulaire, apporte de nombreux nouveaux concepts. Je me souviens encore de la première fois que je l'ai découvert, il y a sept ans, après avoir passé une nuit blanche à le digérer.

Tremblement de terre Netflix

Le principe de jvmquake est que lorsque les choses tournent mal avec la JVM, vous voulez qu'elle meure et ne se bloque pas. Il y a quelques années, j'exécutais des simulations sur un cluster HTCondor soumis à des contraintes de mémoire strictes, et parfois, les tâches restaient bloquées en raison d'erreurs de « mémoire insuffisante ».


Cette bibliothèque force la JVM à mourir, vous permettant de gérer l'erreur réelle. Dans ce cas précis, HTCondor replanifierait automatiquement le travail.

Dernières pensées

Le code qui m'a fait écrire ce post ? J'ai écrit bien pire. Je le fais encore. Le mieux que nous puissions espérer est de continuer à faire moins de erreurs.


Je m'attends à être mécontent en regardant mon propre code dans quelques années.


Et c'est bon signe.



Modifications et merci :


Également publié sur wasteofserver.com