8
votes

La latence d'allocation semble élevée, pourquoi?

J'ai une application (Java) qui fonctionne dans un environnement de latence faible, elle traite généralement des instructions dans ~ 600Micros (+/- 100). Naturellement, comme nous avons progressé plus loin dans l'espace microsecond, les choses que vous voyez que le changement de la latence coûte et que nous avons maintenant remarqué que 2/3 de ce temps est passé dans l'affectation des 2 objets de domaine principal.

Benchmarking a isolé les sections d'incrimination du code à la construction des objets des références existantes, c'est-à-dire essentiellement une charge de références (~ 15 dans chaque classe) et quelques listes nouvelles, bien que voir la note ci-dessous sur exactement Qu'est-ce qui est mesuré ici.

Chacun prend systématiquement ~ 100 Micros qui est inexplicable pour moi et j'essaie de suivre pourquoi. Un indice de référence rapide suggère qu'un objet de taille similaire rempli de chaînes prend environ 2-3Micros à nouveau en hausse, évidemment, ce type de référence est affreux avec difficulté mais que cela pourrait être utile comme une base de référence.

Il y a 2 qs ici

  • Comment enquête-t-il à ce type de comportement?
  • Quelles explications sont là pour une allocation lente?

    Remarque Le matériel impliqué est Solaris 10 x86 sur Sun X4600S avec 8 * Dual Core Opterons @ 3.2GHz

    choses que nous avons examinées incluent

    • Vérification des statistiques d'impression d'impression, montre que v Peu d'allocations lentes, donc il ne devrait y avoir aucune avance.
    • ImprimerCompilation suggère que l'un de ces bits de code n'est pas jit sympathique, bien que Solaris semble avoir un comportement inhabituel ici (Viz a Vis un Linux moderne, n'a pas de vintage similaire à Solaris10 au banc en ce moment) < / li>
    • LogCompilation ... Un peu plus difficile d'analyser pour dire le moins de sorte qu'il s'agit d'un travail continu, rien d'évident jusqu'à présent
    • Versions JVM ... cohérentes à travers 6U6 et 6U14, pas essayé 6U18 ou les plus récents 7 pourtant

      Toute pensée et toutes les pensées appréciées

      Résumé des commentaires sur les postes assortis pour essayer de rendre les choses plus claires
      • Le coût que je mesure est le coût total de la création de l'objet construit via un constructeur (comme l'un des ces ) et dont le constructeur privé invoque une nouvelle arracheliste à quelques reprises, ainsi que de définir des références aux objets existants. Le coût mesuré couvre le coût de la configuration du constructeur et de la conversion du constructeur vers un objet de domaine
      • La compilation (par hotspot) a un impact marqué, mais il est encore relativement lent (la compilation dans ce cas le prend de 100 Micros jusqu'à ~ 60)
      • Compilation (par hotspot) sur mon repère naïf prend le temps d'attribution de ~ 2Micros à ~ 300ns
      • la latence ne varie pas avec la collection de la jeune génération Algo (par novice ou une scène parallèle)


9 commentaires

Votre question est extrêmement verbeuse, mais je comprends-je correctement que vous vous demandez pourquoi il faut 150 μs pour instancier une liste? Si oui, quelle liste implémentatrise la mise en œuvre? Et qu'est-ce qu'un "charge de référence"?


Pourriez-vous poster (une partie de) le code fautif?


> Benchmarking a isolé les sections d'incrimination du code à la construction des objets des références existantes, c'est-à-dire une charge de références que vous pouvez fournir le code?


Je veux dire que j'ai une classe qui a un certain nombre d'attributs (quelques cordes, un couple d'énums, quelques longs, d'autres objets de domaine) qui ont déjà été alloués / complètement construits et tout le constructeur (vraiment un constructeur qui appelle Un CTOR privé) est défini par les membres de la classe pour pointer à ces références et crée également quelques arraylistes vides. Il n'y a pas de "travail" en cours de construction de cet objet.


Je ne sais pas comment poster le code de manière significative tbh


Avez-vous essayé d'analyser la construction de votre objet de domaine avec des références de tableau existantes au lieu d'attribuer de nouveaux?


À titre de comparaison, l'instanciation d'une arracheliste prend environ 30NS sur mon système, qui est une commande de 4000 de réduction sur les résultats que vous parlez. Sans que vous fournissez plus de détails sur votre configuration (utilisez-vous peut-être une sorte d'instrumentation, des aspects ou similaires), je suppose que personne n'est vraiment capable de vous aider.


La collecte des ordures pourrait-elle jouer une partie à cela?


Aucun instrumentation ou aspects impliqué, une référence naïve (allouant un objet consommé par un autre filetage d'une autre évasion) de quelque chose comme une taille similaire montre le temps d'allouer comme environ 2Micros qui tombe sur <300n lorsque l'appel est compilé.


5 Réponses :


2
votes

L'allocation de la mémoire peut provoquer des effets secondaires. Est-il possible que l'allocation de mémoire provoque la compacte du tas? Avez-vous cherché à voir si votre répartition de la mémoire cause-t-elle que le GC fonctionne en même temps?

Avez-vous chronométré séparément combien de temps il faut pour créer les nouveaux arraylistes?


0 commentaires

2
votes

Il n'y a probablement aucun espoir dans l'attente des garanties de la latence microsecondes à partir d'une machine virtuelle générale en cours d'exécution sur un système d'exploitation à usage général, même avec un si grand matériel. Le débit massif est le meilleur que vous puissiez espérer. Que diriez-vous de passer à une machine virtuelle en temps réel si vous en avez besoin d'un (je parle de RTSJ et tout ce que ...)

... mes deux cents


1 commentaires

C'est vrai mais nous n'avons pas vraiment besoin de besoins en temps réel (généralement aussi vite que possible, c'est assez bon) et RTSJ est plutôt invasif AFAIK. De plus, aussi longtemps que je peux expliquer où le temps que je suis passé, alors je suis heureux. Dans ce cas, il est inexplicablement lent, donc j'ai besoin de le comprendre sinon nous avons un comportement inconnu. Le comportement inconnu IME a tendance à entraîner des problèmes plus loin dans la ligne.



3
votes

Étant donné que votre question était davantage sur la façon d'enquêter sur le problème plutôt que de «quel est mon problème», je vais rester avec des outils à essayer.

Un outil très utile pour avoir une meilleure idée de ce qui se passe et quand est Btrace . Il est similaire à Dtrace, mais un outil pure Java. Sur cette note, je suppose que vous savez que DTrace, sinon cela est également utile s'il n'est pas obtus. Celles-ci vous donneront une certaine visibilité sur ce qui se passe et quand dans la JVM et le système d'exploitation.

Oh, une autre chose à clarifier dans votre publication originale. Quel collecteur fonctionnez-vous? Je suppose avec un problème de latence élevé que vous utilisez un collecteur de pause faible comme CMS. Si oui, avez-vous essayé un réglage?


1 commentaires

Oui, nous utilisons CMS. Je n'ai pas jusqu'à présent été tachée pour la performance d'allocation jusqu'à présent, uniquement pour les temps de pause globale. Une autre partie de la syntonisation GC avec la performance d'allocation à l'esprit est sur la liste. Btrace a l'air intéressant, va essayer cela. Dtrace Malheureusement n'est pas autorisé dans notre environnement grâce à notre groupe d'ingénierie toujours utile. Sad & Bizarre mais vrai.



2
votes

juste quelques suppositions sauvages:

Je crois comprendre que Java VMS gère la mémoire d'objets de courte durée de vie différente des objets à long terme. Cela me semblerait raisonnable que, au point où un objet passe d'une référence à une seule fonction - une référence locale à avoir des références dans le Heap mondial serait un grand événement. Au lieu d'être disponible pour le nettoyage à la sortie de la fonction, il doit maintenant être suivi par le GC.

ou il pourrait s'agir d'une référence à plusieurs références à un seul objet doit modifier la comptabilité GC. Tant qu'un objet a une seule référence, il est facile de nettoyer. Plusieurs références peuvent avoir des boucles de référence et / ou le GC peuvent avoir à rechercher la référence dans tous les autres objets.


3 commentaires

Chaque objet est géré par GC & Allocation doit être assez bon marché que, au point, l'objet est attribué, il est attribué à partir d'un tampon local de fil préallocate (TLAB) est donc essentiellement une bosse l'événement du pointeur. Il y a un coût pour une référence intergénérationnelle, mais cela est, Afaik, petit au point de création et entraîne un coût au moment de la collecte en raison du coût accru de déterminer ce qui est en direct ou non. Sur le 2e point, on dirait que vous parlez de référence de référence que les JVM ne font pas.


MATT: En ce qui concerne le comptage de référence: il y a un comptage de référence et il y a un comptage de référence ... Un GC qui connaît la différence entre 1 et de nombreuses références peuvent rendre les choses beaucoup plus faciles à soi-même. Lors du nettoyage des objets qui contiennent la seule référence à un autre objet, cet objet peut également être immédiatement collecté.


à droite, mais le coût (ou la sauvegarde) dans ce cas serait au temps de collecte par opposition au temps d'attribution



3
votes

Lorsque vous répétez la même tâche à plusieurs reprises, votre CPU a tendance à fonctionner très efficacement. En effet, votre cache manquait des temps et votre réchauffement de la CPU n'apparaît pas comme facteur. Sa est également possible que vous ne considérez pas non plus votre temps à chaud JVM.

Si vous essayez la même chose lorsque la JVM et / ou la CPU ne sont pas réchauffées. Vous obtiendrez des résultats très différents. P>

Essayez de faire la même chose dire 25 fois (moins que votre seuil de compilation) et dormez (100) entre les tests. Vous devriez vous attendre à voir des moments beaucoup plus élevés, plus près de ce que vous voyez dans la demande réelle. P>

Le comportement de votre application va différer mais pour illustrer mon point. J'ai trouvé attendre que io puisse être plus perturbateur qu'un sommeil clair. p>

Lorsque vous effectuez votre référence, vous devez essayer de vous assurer de comparer comme avec comme. P>

import java.io.*;
import java.util.Date;

/**
Cold JVM with a Hot CPU took 123 us average
Cold JVM with a Cold CPU took 403 us average
Cold JVM with a Hot CPU took 314 us average
Cold JVM with a Cold CPU took 510 us average
Cold JVM with a Hot CPU took 316 us average
Cold JVM with a Cold CPU took 514 us average
Cold JVM with a Hot CPU took 315 us average
Cold JVM with a Cold CPU took 545 us average
Cold JVM with a Hot CPU took 321 us average
Cold JVM with a Cold CPU took 542 us average
Hot JVM with a Hot CPU took 44 us average
Hot JVM with a Cold CPU took 111 us average
Hot JVM with a Hot CPU took 32 us average
Hot JVM with a Cold CPU took 96 us average
Hot JVM with a Hot CPU took 26 us average
Hot JVM with a Cold CPU took 80 us average
Hot JVM with a Hot CPU took 26 us average
Hot JVM with a Cold CPU took 90 us average
Hot JVM with a Hot CPU took 25 us average
Hot JVM with a Cold CPU took 98 us average
 */
public class HotColdBenchmark {
    public static void main(String... args) {
        // load all the classes.
        performTest(null, 25, false);
        for (int i = 0; i < 5; i++) {
            // still pretty cold
            performTest("Cold JVM with a Hot CPU", 25, false);
            // still pretty cold
            performTest("Cold JVM with a Cold CPU", 25, true);
        }

        // warmup the JVM
        performTest(null, 10000, false);
        for (int i = 0; i < 5; i++) {
            // warmed up.
            performTest("Hot JVM with a Hot CPU", 25, false);
            // bit cold
            performTest("Hot JVM with a Cold CPU", 25, true);
        }
    }

    public static long performTest(String report, int n, boolean sleep) {
        long time = 0;
        long ret = 0;
        for (int i = 0; i < n; i++) {
            long start = System.nanoTime();
            try {
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                ObjectOutputStream oos = new ObjectOutputStream(baos);
                oos.writeObject(new Date());
                oos.close();
                ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
                Date d = (Date) ois.readObject();
                ret += d.getTime();
                time += System.nanoTime() - start;
                if (sleep) Thread.sleep(100);
            } catch (Exception e) {
                throw new AssertionError(e);
            }
        }
        if (report != null) {
            System.out.printf("%s took %,d us average%n", report, time / n / 1000);
        }
        return ret;
    }
}


8 commentaires

Totalement d'accord mais je suis convaincu que ce n'est pas le problème dans ce cas car j'ai fait de longues repères (> 24h) et des repères répétés. Cela signifie que N * 30 pistes de 20 minutes supérieures au temps de compilation et rapportant les résultats agrégés, c'est-à-dire en rapportant des performances toutes les ns, puis résumant les résultats par période (ce qui signifie une étape équivalente dans la course) avec min / max / avg et ainsi au. Les résultats sont cohérents une fois la compilation de la compilation. Il s'agit également d'une référence d'application non synthétique, une charge est donc représentative.


Est-ce que vous cpu chaud ou froid pour la période chronométrée? C'est-il bloquer ou dormir avant la période chronométrée?


Le (s) serveur (s) est généralement dans les mois et il s'agit d'une référence d'application afin qu'elle fonctionne dans des conditions normales, je ne contrôle pas directement l'état des noyaux avant le début du test. Ils seront surtout inactifs. Étant donné qu'il y a 16 noyaux sur la boîte et aucun jeu de processeur en place et que le planificateur Solaris a tendance à déplacer les LWPS autour de cela, car il est tout à fait impossible de contrôler l'état d'un noyau unique.


Donc, si votre CPU est généralement froide, vous devez compenser la référence de la manière dont votre application passe à froid pour obtenir des résultats comparables. J'ai trouvé la latence augmente 2-5X dans ce cas. BTW, ne supposez pas qu'il n'y a rien que vous puissiez faire à ce sujet. ;)


Les résultats sont répétables sur de nombreuses exécutions lorsque l'application est soumise à une charge normale. Il n'appelle jamais thread.sleep (sauf au démarrage), en général, les threads de traitement seront actifs ou en attente de travail (alias non sécurisé.park via une liaisonTransferqueur.take). Je vous manquerai peut-être votre point, mais votre exemple me dit: «Faites de votre mieux pour vous forcer à vous forcer à la CPU et que vous ne cédez jamais pour garder les choses à filer. Je ne vois pas comment c'est viable dans une vraie application à moins que vous n'ayez le tout. Boîte à un seul processus, malheureusement pas un luxe que j'ai!


Cela vaut également la peine de garder à l'esprit que la particularité du code que je suis préoccupé est au milieu d'une autre autre transformation par conséquent, le thread est activement sur la CPU (étant donné que nous ne surchargons pas la boîte, il est rare d'avoir un fil actif heurté) et fait une variété de travaux (liés) par opposition à un peu spécifique dont je parle ici


Vous devez décider s'il vaut la peine de consacrer un fil / un processeur à une tâche en attendant ou non. Si vous dédiez la CPU, vous obtiendrez de meilleurs horaires de latence, si vous ne le faites pas, la CPU peut être utilisée pour effectuer d'autres tâches (d'après ce que vous dites que cela suggère que cela n'est pas vraiment nécessaire) si la latence est une véritable préoccupation, vous ne devrait pas réussir le travail entre les threads en aucun cas.


Il ne passe pas des unités de travail à travers les threads, mais je dois équilibrer la latence contre le débit, alors attacher un fil comme celui-là n'est pas vraiment réalisable.