- In OpenJDK wurde
ThreadMXBean.getCurrentThreadUserTime()durch einenclock_gettime()-Aufruf statt/proc-Dateiparsing ersetzt und erreicht damit eine bis zu 400-fache Performance-Steigerung - Die bisherige Implementierung durchlief einen komplexen I/O-Pfad, bei dem die Datei
/proc/self/task/<tid>/statgeöffnet, gelesen und geparst wurde - Die neue Implementierung nutzt die Bit-Kodierung von
clockid_tim Linux-Kernel: Durch Anpassen der unteren Bits einer perpthread_getcpuclockid()erhaltenen ID kann direkt nur die User-Zeit abgefragt werden - Laut Benchmark sank die durchschnittliche Aufrufzeit von 11μs auf 279ns; nach Anwendung eines Kernel-Fast-Path folgte eine zusätzliche Verbesserung um etwa 13 %
- Das ist ein Beispiel dafür, wie durch Verständnis der internen Linux-ABI über POSIX-Einschränkungen hinaus Optimierungen möglich werden
Probleme der bisherigen Implementierung
getCurrentThreadUserTime()öffnete die Datei/proc/self/task/<tid>/statund parste das 13. und 14. Feld, um die CPU-User-Zeit zu berechnen- Dafür waren mehrere Verarbeitungsschritte nötig: Pfad erzeugen, Datei öffnen, Buffer lesen, String parsen,
sscanf()aufrufen - Weil der Befehlsname Klammern enthalten kann, war zudem komplizierte Logik mit
strrchr()nötig, um das letzte)zu finden
- Dafür waren mehrere Verarbeitungsschritte nötig: Pfad erzeugen, Datei öffnen, Buffer lesen, String parsen,
getCurrentThreadCpuTime()führte dagegen nur einen einzelnen Aufruf vonclock_gettime(CLOCK_THREAD_CPUTIME_ID)aus- Laut einem Bug-Report aus dem Jahr 2018 (JDK-8210452) betrug der Geschwindigkeitsunterschied zwischen den beiden Methoden 30- bis 400-fach
Vergleich von /proc-Zugriffspfad und clock_gettime()-Pfad
- Der
/proc-Ansatz umfasst mehrere Systemaufrufe und interne String-Erzeugung im Kernel, darunteropen(),read(),sscanf()undclose() - Der
clock_gettime()-Ansatz liest den Zeitwert per einzelnem Systemaufruf direkt aus der Struktursched_entity - Unter paralleler Last verstärkt sich die Verzögerung beim
/proc-Zugriff durch Lock-Contention im Kernel
Neue Implementierung
- Der POSIX-Standard definiert, dass
CLOCK_THREAD_CPUTIME_IDUser- plus Systemzeit zurückgibt - Der Linux-Kernel kodiert den Typ der Uhr in den unteren Bits von
clockid_t00=PROF,01=VIRT(nur User-Zeit),10=SCHED(User+System)
- Wenn bei einer mit
pthread_getcpuclockid()erhaltenenclockiddie unteren Bits auf01gesetzt werden, lässt sich auf eine nur für User-Zeit zuständige Uhr umschalten - Im neuen Code entfallen Datei-I/O und Parsing; die User-Zeit wird nur noch über
clock_gettime()zurückgegeben
Ergebnisse der Performance-Messung
- Vor der Änderung lag die durchschnittliche Aufrufzeit bei 11,186μs, danach bei 0,279μs - eine Verbesserung um etwa das 40-Fache
- Gemessen in einer Umgebung mit 16 Threads, im Einklang mit der ursprünglich gemeldeten Spanne von 30- bis 400-fach
- Im CPU-Profil verschwanden die Systemaufrufe zum Öffnen und Schließen von Dateien; übrig blieb nur ein einzelner
clock_gettime()-Aufruf
Zusätzliche Optimierung durch Kernel-Fast-Path
- Der Kernel bietet einen Fast-Path, wenn in
clockidPID=0 kodiert ist und damit direkt auf den aktuellen Thread zugegriffen werden kann - Wenn die JVM die
clockiddirekt konstruiert stattpthread_getcpuclockid()zu verwenden und dabei PID=0 einsetzt, kann die Radix-Tree-Suche übersprungen werden - Bei Verwendung einer manuell konstruierten
clockidsank die durchschnittliche Zeit von 81,7ns auf 70,8ns, also um weitere etwa 13 % - Allerdings besteht die Gefahr von Einbußen bei Lesbarkeit und Kompatibilität, da dies von Kernel-internen Implementierungsdetails wie der Größe von
clockid_tabhängt
Fazit und Lehren
- Durch das Löschen von 40 Zeilen wurde ein 400-facher Performance-Unterschied beseitigt, ganz ohne neue Kernel-Funktionalität, allein durch Nutzung von Details der bestehenden ABI
- Hervorgehoben wird der Wert des Studiums des Kernel-Quellcodes: POSIX garantiert Portabilität, aber der Kernel-Code zeigt die Grenzen des Möglichen
- Wichtig ist auch, bestehende Annahmen neu zu prüfen: Das Parsen von
/procwar früher sinnvoll, ist heute aber ineffizient - Die Änderung wird in JDK 26 enthalten sein, das voraussichtlich im März 2026 erscheint, und sorgt damit bei Aufrufen von
ThreadMXBean.getCurrentThreadUserTime()automatisch für bessere Performance
5 Kommentare
Eigentlich ist es schon schwierig genug, den Code komplett neu zu schreiben und dabei nur eine 2- bis 3-fache Verbesserung zu erzielen – umso beeindruckender ist es, mit nur ein paar geänderten Zeilen eine bis zu 400-fache Leistungssteigerung zu erreichen.
Hacker-News-Kommentare
Dabei habe ich festgestellt, dass die Frage „Wie hoch ist die CPU-Nutzungszeit dieses Threads?“ eine erstaunlich teure Operation ist
Ohne einen Maßstab auf Atomuhren-Niveau ist es meiner Meinung nach schwer, absolute Werte zu behaupten
clock_gettime()vermeidet über vDSO einen Kontextwechsel. Deshalb ist das auch im Flamegraph sichtbarCLOCK_VIRToderCLOCK_SCHEDist weiterhin ein Syscall-Aufruf nötigCLOCK_THREAD_CPUTIME_IDlandet letztlich doch im Kernel, weil auf die Task-Struct zugegriffen werden muss.Relevanter Kernel-Quellcode: posix-cpu-timers.c,
cputime.c,
gettimeofday.c
PERF_COUNT_SW_TASK_CLOCKsind auch Messungen von etwa 8 ns möglich.Gelesen wird dabei über
perf_event_mmap_pageaus einer Shared Page, und die Differenz wird perrdtsc-Aufruf berechnet.Das ist schlecht dokumentiert und es gibt kaum Open-Source-Implementierungen
perf_event-Konfiguration und der nötigen Berechtigungen scheint das aber eher für langlebige Threads geeignet zu seinseqlocknötig ist. Vielleicht, um zu verhindern, dass zwischen dem Seitenwert undrdtscein Kontextwechsel stattfindet?Vermutlich wird nach
rdtscder Seitenwert noch einmal geprüft und bei Änderungen erneut versucht.Zur Einordnung: Auch
clock_gettimeist ein virtueller Syscall auf vdso-Basisclock_gettimeist kein Syscall, sondern nutzt vdsoIm Code sieht oft alles in Ordnung aus, aber im Flamegraph denkt man dann: „Was ist das denn?!“
So wurden schon viele Probleme entdeckt, etwa Initialisierung statt statischer Initialisierung oder ein einzeiliger Logger-Aufruf, der teure Serialisierung auslöst
Normalerweise nutze ich den HTML-Generator von async-profiler, aber diesmal habe ich für ein einzelnes SVG Brendans Tool verwendet
/proc, eBPF-Profiling und die Geschichte der schlecht dokumentierten User-Space-ABI geschrieben.Mehr Details stehen in meinem Blogbeitrag
Tweet als Quelle
Auch Jaromirs Blog war wirklich klasse
Wie kann man so etwas in einem Projekt entdecken? Ich glaube, allein dadurch, dass man KI laufen lässt, ist das schwer zu erkennen..
Wenn ich solche Beispiele sehe, denke ich, dass ich das auch lernen und unbedingt selbst einmal erleben möchte.
Beeindruckend.
Ich finde, das ist nicht völlig falsch, aber wenn der Kernel mit im Spiel ist, war es vermutlich schon extrem schwierig, überhaupt zu bemerken, wo die Langsamkeit herkam.