- In OpenJDK wurde
ThreadMXBean.getCurrentThreadUserTime() durch einen clock_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>/stat geöffnet, gelesen und geparst wurde
- Die neue Implementierung nutzt die Bit-Kodierung von
clockid_t im Linux-Kernel: Durch Anpassen der unteren Bits einer per pthread_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>/stat und 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
getCurrentThreadCpuTime() führte dagegen nur einen einzelnen Aufruf von clock_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, darunter open(), read(), sscanf() und close()
- Der
clock_gettime()-Ansatz liest den Zeitwert per einzelnem Systemaufruf direkt aus der Struktur sched_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_ID User- plus Systemzeit zurückgibt
- Der Linux-Kernel kodiert den Typ der Uhr in den unteren Bits von
clockid_t
00=PROF, 01=VIRT (nur User-Zeit), 10=SCHED (User+System)
- Wenn bei einer mit
pthread_getcpuclockid() erhaltenen clockid die unteren Bits auf 01 gesetzt 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
clockid PID=0 kodiert ist und damit direkt auf den aktuellen Thread zugegriffen werden kann
- Wenn die JVM die
clockid direkt konstruiert statt pthread_getcpuclockid() zu verwenden und dabei PID=0 einsetzt, kann die Radix-Tree-Suche übersprungen werden
- Bei Verwendung einer manuell konstruierten
clockid sank 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_t abhä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
/proc war 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
Noch keine Kommentare.