30 points par GN⁺ 2026-01-15 | 5 commentaires | Partager sur WhatsApp
  • Dans OpenJDK, ThreadMXBean.getCurrentThreadUserTime() a été remplacé par un appel à clock_gettime() au lieu d’un parsing de fichier /proc, avec à la clé jusqu’à 400x de gain de performances
  • L’implémentation précédente suivait un chemin d’E/S complexe en ouvrant, lisant et analysant le fichier /proc/self/task/<tid>/stat
  • La nouvelle implémentation exploite l’encodage binaire de clockid_t dans le noyau Linux pour ajuster les bits de poids faible de l’identifiant obtenu via pthread_getcpuclockid() et interroger directement uniquement le temps utilisateur
  • Les benchmarks montrent que le temps moyen par appel passe de 11μs à 279ns, puis environ 13 % de mieux après l’application d’un fast-path côté noyau
  • C’est un exemple qui montre qu’une optimisation est possible au-delà des contraintes de POSIX grâce à une compréhension de l’ABI interne de Linux

Problèmes de l’implémentation existante

  • getCurrentThreadUserTime() ouvrait le fichier /proc/self/task/<tid>/stat pour analyser les 13e et 14e champs et calculer le temps CPU utilisateur
    • cela nécessitait plusieurs étapes : construction du chemin, ouverture du fichier, lecture dans un tampon, parsing de chaîne, appel à sscanf(), etc.
    • le nom de commande pouvant contenir des parenthèses, la logique incluait aussi une recherche complexe du dernier ) avec strrchr()
  • À l’inverse, getCurrentThreadCpuTime() n’effectuait qu’un seul appel à clock_gettime(CLOCK_THREAD_CPUTIME_ID)
  • D’après le rapport de bug de 2018 (JDK-8210452), l’écart de vitesse entre les deux méthodes atteignait 30 à 400x

Comparaison entre le chemin d’accès /proc et le chemin clock_gettime()

  • La méthode /proc inclut plusieurs appels système et la génération de chaînes dans le noyau, comme open(), read(), sscanf() et close()
  • La méthode clock_gettime() repose sur un seul appel système qui lit directement la valeur temporelle depuis la structure sched_entity
  • Sous charge parallèle, l’accès à /proc subit des ralentissements accrus à cause de la contention sur les verrous du noyau

Nouvelle méthode d’implémentation

  • Le standard POSIX définit que CLOCK_THREAD_CPUTIME_ID doit renvoyer le temps utilisateur + le temps système
  • Le noyau Linux encode le type d’horloge dans les bits de poids faible de clockid_t
    • 00=PROF, 01=VIRT(utilisateur uniquement), 10=SCHED(utilisateur + système)
  • En remplaçant les bits de poids faible du clockid obtenu via pthread_getcpuclockid() par 01, il devient possible de basculer vers une horloge dédiée uniquement au temps utilisateur
  • Le nouveau code supprime les E/S fichier et le parsing, et renvoie le temps utilisateur via un simple appel à clock_gettime()

Résultats des mesures de performances

  • Avant la modification, le temps moyen par appel était de 11,186μs ; après, il tombe à 0,279μs, soit une amélioration d’environ 40x
    • mesure réalisée dans un environnement à 16 threads, cohérente avec la fourchette initialement rapportée de 30 à 400x
  • Dans les profils CPU, les appels système liés à l’ouverture et à la fermeture de fichiers disparaissent, ne laissant qu’un seul appel à clock_gettime()

Optimisation supplémentaire avec le fast-path du noyau

  • Le noyau fournit un fast-path qui accède directement au thread courant lorsque le clockid encode PID=0
  • Si la JVM construit directement le clockid au lieu d’utiliser pthread_getcpuclockid(), en y injectant PID=0, elle peut éviter la recherche dans l’arbre radix
  • Avec un clockid construit manuellement, le temps moyen passe de 81,7ns à 70,8ns, soit environ 13 % d’amélioration supplémentaire
  • En contrepartie, cela dépend de détails d’implémentation internes du noyau, comme la taille de clockid_t, avec un risque de perte de lisibilité et de compatibilité

Conclusion et enseignements

  • La suppression de 40 lignes a éliminé un écart de performances de 400x, sans nouvelle fonctionnalité noyau, uniquement en exploitant la structure détaillée de l’ABI existante
  • L’article souligne la valeur d’une lecture attentive du code source du noyau : POSIX garantit la portabilité, mais le code du noyau montre les limites du possible
  • L’importance de reconsidérer les hypothèses existantes : analyser /proc était autrefois raisonnable, mais c’est aujourd’hui inefficace
  • Ce changement sera intégré à JDK 26 (sortie prévue en mars 2026) et apportera un gain de performances automatique lors des appels à ThreadMXBean.getCurrentThreadUserTime()

5 commentaires

 
aobamisaki 2026-01-15

En réalité, même en refondant complètement tout le code, obtenir un gain de 2 à 3 fois est déjà difficile ; alors atteindre jusqu’à 400 fois plus de performances en ne changeant que quelques lignes, c’est vraiment impressionnant.

 
GN⁺ 2026-01-15
Commentaires sur Hacker News
  • C’est moi l’auteur. Après mon précédent billet sur un bug du kernel, j’ai examiné la façon dont la JVM rapporte elle-même l’activité des threads
    Je me suis rendu compte que la question « quel est le temps CPU utilisé par ce thread ? » est une opération bien trop coûteuse en pratique
    • Pour parler de mesures à la nanoseconde près, il faut très bien comprendre la stabilité et la précision de l’horloge
      Sans référence de niveau horloge atomique, il me semble difficile d’affirmer des valeurs absolues
    • Je me demande si l’auteur a examiné la raison pour laquelle la distribution s’étale sur plusieurs ordres de grandeur. C’est un phénomène intéressant en soi
    • Merci vraiment pour le résumé TL;DR. Ce type de résumé abaisse la barrière d’entrée d’un article et donne envie de le lire
    • Réaction : « pas surprenant (Quelle Surprise) »
  • clock_gettime() évite le changement de contexte via le vDSO. On en voit donc la trace dans la flamegraph
    • Mais cela ne concerne que certaines horloges. Pour CLOCK_VIRT ou CLOCK_SCHED, par exemple, il faut toujours passer par un appel système
    • Si on regarde sous la frame vDSO, on voit encore un syscall. Il semble qu’aucun chemin rapide (fast path) ne soit implémenté pour certains clock id
    • CLOCK_THREAD_CPUTIME_ID finit bien par passer dans le kernel, car il faut consulter la task struct
      Voir le code source du kernel correspondant : posix-cpu-timers.c,
      cputime.c,
      gettimeofday.c
  • Avec PERF_COUNT_SW_TASK_CLOCK, il est possible d’obtenir des mesures autour de 8 ns
    Le principe consiste à lire depuis une page partagée via perf_event_mmap_page, puis à calculer le delta avec un appel à rdtsc
    C’est peu documenté et il n’existe pratiquement pas d’implémentations open source
    • C’est une astuce vraiment élégante. En revanche, vu les contraintes de configuration de perf_event et les permissions nécessaires, cela semble surtout adapté à des threads de longue durée de vie
    • Quelqu’un demande pourquoi un seqlock est nécessaire. Est-ce pour éviter qu’un changement de contexte ne se produise entre la valeur de la page et rdtsc ?
      A priori, la structure semble consister à revérifier la valeur de la page après rdtsc et à réessayer si elle a changé
      À noter que clock_gettime est lui aussi un syscall virtuel basé sur vdso
    • clock_gettime n’est pas un syscall, il utilise le vdso
  • La flamegraph est vraiment un outil formidable
    En regardant seulement le code, tout peut sembler correct, mais avec une flamegraph on se dit souvent : « mais qu’est-ce que c’est que ça ?! »
    J’y ai déjà découvert divers problèmes, comme des initialisations faites hors initialisation statique, ou un appel de logger d’une ligne qui déclenchait une sérialisation coûteuse
    • J’aime aussi les icicle graphs. Ils s’accumulent dans le sens inverse des flamegraphs, ce qui permet de mieux voir les goulets d’étranglement lorsque plusieurs chemins appellent une bibliothèque commune
    • Si vous ouvrez cet exemple SVG dans un nouvel onglet, vous pouvez utiliser le zoom interactif
    • Le profiling de performance et les expériences d’optimisation font partie des aspects les plus amusants du développement. On se retrouve souvent à se demander : « pourquoi est-ce si lent ? »
    • Certains trouvaient étrange l’association entre parsing de chaînes et memoization. En réalité, le problème venait du fait qu’on ne mettait pas en cache l’analyse d’expressions régulières coûteuses
    • Quelqu’un demande les concepts de base et un point de départ pour une première utilisation d’une flamegraph
  • Le fait que « ouvrir l’image dans un nouvel onglet » fournisse réellement une interaction SVG a surpris plusieurs personnes
    • Cette fonctionnalité vient du script FlameGraph de Brendan Gregg
      D’habitude, j’utilise le générateur HTML d’async-profiler, mais cette fois j’ai utilisé l’outil de Brendan pour produire un SVG unique
  • C’est moi l’auteur du patch OpenJDK. J’ai parlé de la surcharge mémoire liée à la lecture de /proc, du profiling eBPF, et de l’histoire d’une ABI user-space peu documentée
    J’ai rassemblé les détails dans un billet sur mon blog
    • On lui demande pourquoi l’implémentation d’origine était faite ainsi. Faire des E/S fichier et du parsing de chaînes à chaque appel est inefficace, mais il devait sans doute y avoir une raison à l’époque
    • Jaromir a lu mon billet et a dit : « moi aussi, j’ai écrit un brouillon à la même période », puis ils ont lié leurs articles respectifs. J’étais content qu’il juge mon article plus rigoureux
  • Le fait d’utiliser un langage système comme le C ou le C++ ne garantit pas d’être rapide. La vitesse dépend énormément de ce qu’on fait réellement
  • Les lectures via le vDSO sont bien plus rapides, car elles évitent la transition vers le kernel, la sérialisation des buffers et le parsing
  • Quelqu’un partage cette citation : « Si c’est allé 2 fois plus vite, c’est peut-être que vous avez fait quelque chose d’intelligent ; si c’est allé 100 fois plus vite, c’est simplement que vous avez arrêté de faire quelque chose de stupide »
    Tweet source
  • L’équipe de QuestDB est au plus haut niveau dans ce domaine. Les personnes comme le logiciel sont excellents
    Le blog de Jaromir était lui aussi remarquable
 
[Ce commentaire a été masqué.]
 
princox 2026-01-19

Comment peut-on repérer ce genre de choses dans un projet ? J’ai l’impression qu’il serait difficile de s’en rendre compte simplement en faisant tourner une IA..

Quand je vois ce type de cas, je me dis que moi aussi j’aimerais apprendre et vivre ce genre d’expérience.

 
crawler 2026-01-15

C’est impressionnant.

Si c’est allé 2 fois plus vite, c’est peut-être qu’on a fait quelque chose d’intelligent ; si c’est allé 100 fois plus vite, c’est juste qu’on a arrêté de faire quelque chose de stupide.

Je pense que ce n’est pas complètement faux, mais quand c’est lié au noyau, je pense que rien que le fait de se rendre compte que c’était lent a dû être vraiment difficile.