- 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()
Aucun commentaire pour le moment.