30 Punkte von GN⁺ 2026-01-15 | 5 Kommentare | Auf WhatsApp teilen
  • 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

5 Kommentare

 
aobamisaki 2026-01-15

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.

 
GN⁺ 2026-01-15
Hacker-News-Kommentare
  • Ich bin der Autor. Nach dem letzten Beitrag über einen Kernel-Bug habe ich mir angesehen, wie die JVM selbst die Thread-Aktivität meldet.
    Dabei habe ich festgestellt, dass die Frage „Wie hoch ist die CPU-Nutzungszeit dieses Threads?“ eine erstaunlich teure Operation ist
    • Wenn man über Messungen im Nanosekundenbereich sprechen will, muss man Stabilität und Genauigkeit der Uhr sehr gut verstehen.
      Ohne einen Maßstab auf Atomuhren-Niveau ist es meiner Meinung nach schwer, absolute Werte zu behaupten
    • Ich frage mich, ob untersucht wurde, warum die Verteilung über mehrere Größenordnungen gestreut ist. Das ist an sich schon ein interessantes Phänomen
    • Ich war wirklich dankbar für die kurze TL;DR-Zusammenfassung. Solche Zusammenfassungen senken die Einstiegshürde und motivieren zum Weiterlesen
    • Jemand reagierte mit „Nicht überraschend (Quelle Surprise)“
  • clock_gettime() vermeidet über vDSO einen Kontextwechsel. Deshalb ist das auch im Flamegraph sichtbar
    • Das gilt aber nur für einige Clocks. Bei CLOCK_VIRT oder CLOCK_SCHED ist weiterhin ein Syscall-Aufruf nötig
    • Unterhalb des vDSO-Frames ist immer noch ein Syscall zu sehen. Für bestimmte Clock-IDs scheint kein Fast Path implementiert zu sein
    • CLOCK_THREAD_CPUTIME_ID landet letztlich doch im Kernel, weil auf die Task-Struct zugegriffen werden muss.
      Relevanter Kernel-Quellcode: posix-cpu-timers.c,
      cputime.c,
      gettimeofday.c
  • Mit PERF_COUNT_SW_TASK_CLOCK sind auch Messungen von etwa 8 ns möglich.
    Gelesen wird dabei über perf_event_mmap_page aus einer Shared Page, und die Differenz wird per rdtsc-Aufruf berechnet.
    Das ist schlecht dokumentiert und es gibt kaum Open-Source-Implementierungen
    • Ein wirklich cooler Trick. Wegen des Aufwands für die perf_event-Konfiguration und der nötigen Berechtigungen scheint das aber eher für langlebige Threads geeignet zu sein
    • Es wurde gefragt, warum seqlock nötig ist. Vielleicht, um zu verhindern, dass zwischen dem Seitenwert und rdtsc ein Kontextwechsel stattfindet?
      Vermutlich wird nach rdtsc der Seitenwert noch einmal geprüft und bei Änderungen erneut versucht.
      Zur Einordnung: Auch clock_gettime ist ein virtueller Syscall auf vdso-Basis
    • clock_gettime ist kein Syscall, sondern nutzt vdso
  • Flamegraphs sind wirklich ein hervorragendes Werkzeug.
    Im 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
    • Ich mag auch Icicle Graphs. Sie kumulieren in die entgegengesetzte Richtung zu Flamegraphs, wodurch sich Engpässe leichter erkennen lassen, wenn mehrere Pfade dieselbe Bibliothek aufrufen
    • Wenn man dieses SVG-Beispiel in einem neuen Tab öffnet, ist interaktives Zoomen möglich
    • Performance-Profiling und Optimierungs-Experimente gehören zu den schönsten Teilen der Entwicklung. Es gibt ständig diese Momente des Staunens: „Warum ist das so langsam?“
    • Es gab auch die Meinung, die Kombination aus String-Parsing und Memoization klinge merkwürdig. Tatsächlich lag das Problem daran, dass teures Parsing von Regex-Mustern nicht gecacht wurde
    • Jemand fragte nach den Grundkonzepten und einem Einstiegspunkt für Leute, die Flamegraphs zum ersten Mal verwenden wollen
  • Es war überraschend, dass „Bild in neuem Tab öffnen“ tatsächlich SVG-Interaktivität ermöglicht.
    • Diese Funktion stammt von Brendan Greggs FlameGraph-Skripten.
      Normalerweise nutze ich den HTML-Generator von async-profiler, aber diesmal habe ich für ein einzelnes SVG Brendans Tool verwendet
  • Ich bin der Autor des OpenJDK-Patches. Ich habe über den Speicher-Overhead beim Lesen von /proc, eBPF-Profiling und die Geschichte der schlecht dokumentierten User-Space-ABI geschrieben.
    Mehr Details stehen in meinem Blogbeitrag
    • Es kam die Frage auf, warum die ursprüngliche Implementierung so war. Datei-I/O und String-Parsing bei jedem Aufruf sind ineffizient, aber ich nehme an, dass es damals einen Grund dafür gab
    • Jaromir schrieb nach der Lektüre meines Beitrags, er habe „zur gleichen Zeit ebenfalls einen Entwurf geschrieben“, und die beiden Beiträge verlinken sich gegenseitig. Ich habe mich gefreut, dass er meinen Beitrag als gründlicher eingeschätzt hat
  • Nur weil etwas in einer Systemprogrammiersprache wie C oder C++ geschrieben ist, ist es nicht automatisch schnell. Die Geschwindigkeit hängt stark davon ab, was man tut
  • Lesen über vDSO ist viel schneller, weil Kernel-Wechsel, Puffer-Serialisierung und Parsing vermieden werden
  • Jemand teilte das Zitat: „Wenn etwas 2x schneller wurde, war es vielleicht klug gemacht; wenn es 100x schneller wurde, hat man wahrscheinlich einfach aufgehört, etwas Dummes zu tun.“
    Tweet als Quelle
  • Das QuestDB-Team gehört in diesem Bereich zur Spitze. Sowohl die Leute als auch die Software sind großartig.
    Auch Jaromirs Blog war wirklich klasse
 
[Dieser Kommentar wurde ausgeblendet.]
 
princox 2026-01-19

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.

 
crawler 2026-01-15

Beeindruckend.

Wenn etwas doppelt so schnell geworden ist, war das vielleicht ein kluger Schachzug; wenn es 100-mal schneller geworden ist, hat man wahrscheinlich nur aufgehört, etwas Dummes zu tun.

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.