30 puan yazan GN⁺ 2026-01-15 | 5 yorum | WhatsApp'ta paylaş
  • OpenJDK'de ThreadMXBean.getCurrentThreadUserTime(), /proc dosyası ayrıştırması yerine clock_gettime() çağrısı ile değiştirilerek en fazla 400 kat performans artışı sağladı
  • Önceki uygulama, /proc/self/task/<tid>/stat dosyasını açıp okuyup ayrıştıran karmaşık bir I/O yolundan geçiyordu
  • Yeni uygulama, Linux çekirdeğinin clockid_t bit kodlamasını kullanarak pthread_getcpuclockid() ile alınan kimliğin alt bitlerini ayarlıyor ve yalnızca user time'ı doğrudan sorguluyor
  • Benchmark sonuçlarına göre ortalama çağrı süresi 11μs → 279ns seviyesine indi; ardından çekirdek fast-path uygulanınca yaklaşık %13 ek iyileşme görüldü
  • Bu, POSIX kısıtlarının ötesine geçip Linux iç ABI'sini anlayarak optimizasyon yapılabileceğini gösteren bir örnek

Mevcut uygulamanın sorunu

  • getCurrentThreadUserTime(), CPU user time'ı hesaplamak için /proc/self/task/<tid>/stat dosyasını açıp 13. ve 14. alanları ayrıştırıyordu
    • Dosya yolu oluşturma, dosya açma, tampon okuma, dize ayrıştırma, sscanf() çağrısı gibi çok aşamalı işlemler gerekiyordu
    • Komut adında parantez bulunabildiği için son ) karakterini strrchr() ile arayan karmaşık bir mantık da vardı
  • Buna karşılık getCurrentThreadCpuTime(), yalnızca tek bir clock_gettime(CLOCK_THREAD_CPUTIME_ID) çağrısı yapıyordu
  • 2018 tarihli hata raporuna (JDK-8210452) göre iki metod arasındaki hız farkı 30 ila 400 kat arasındaydı

/proc erişim yolu ile clock_gettime() yolunun karşılaştırması

  • /proc yöntemi; open(), read(), sscanf(), close() gibi birden fazla sistem çağrısı ve çekirdek içinde dize oluşturma işlemleri içeriyor
  • clock_gettime() yöntemi ise tek bir sistem çağrısı ile sched_entity yapısından zaman değerini doğrudan okuyor
  • Paralel yük altında /proc erişimi, çekirdek kilidi rekabeti nedeniyle daha da fazla gecikmeye uğruyor

Yeni uygulama yöntemi

  • POSIX standardı, CLOCK_THREAD_CPUTIME_ID değerinin user+system time döndürmesini tanımlar
  • Linux çekirdeği, clockid_t alt bitleriyle saat türünü kodlar
    • 00=PROF, 01=VIRT(yalnızca user), 10=SCHED(user+system)
  • pthread_getcpuclockid() ile alınan clockid değerinin alt bitlerini 01 olarak değiştirince yalnızca user time için saat elde edilebiliyor
  • Yeni kod, dosya I/O'sunu ve ayrıştırmayı kaldırıp user time'ı yalnızca clock_gettime() çağrısıyla döndürüyor

Performans ölçüm sonuçları

  • Değişiklikten önce ortalama çağrı süresi 11.186μs, değişiklikten sonra 0.279μs oldu; yani yaklaşık 40 kat iyileşme sağlandı
    • Ölçüm 16 thread ortamında yapıldı ve bu sonuç, başlangıçta bildirilen 30–400 kat aralığıyla uyumlu
  • CPU profilinde dosya açma-kapama ile ilgili sistem çağrıları ortadan kalktı ve geriye yalnızca tek bir clock_gettime() çağrısı kaldı

Çekirdek fast-path ile ek optimizasyon

  • Çekirdek, clockid içine PID=0 kodlandığında mevcut thread'e doğrudan erişen bir fast-path sağlıyor
  • JVM, pthread_getcpuclockid() yerine clockid değerini doğrudan oluşturup PID=0 koyarsa radix tree aramasını atlayabiliyor
  • Elle oluşturulmuş clockid kullanıldığında ortalama süre 81.7ns → 70.8ns oldu; bu da yaklaşık %13 ek iyileşme anlamına geliyor
  • Ancak bu yaklaşım, clockid_t boyutu gibi çekirdek içi uygulama ayrıntılarına bağlı olduğundan okunabilirlik ve uyumluluk kaybı riski taşıyor

Sonuç ve çıkarımlar

  • 40 satırın silinmesiyle 400 katlık performans farkı ortadan kalktı; bu, yeni bir çekirdek özelliği eklemeden, yalnızca mevcut ABI'nin ayrıntılı yapısından yararlanılarak başarıldı
  • Çekirdek kaynak kodunu incelemenin değeri vurgulanıyor: POSIX taşınabilirliği garanti eder, ancak çekirdek kodu nelerin mümkün olduğunun sınırlarını gösterir
  • Mevcut varsayımları yeniden gözden geçirmenin önemi ortaya çıkıyor: /proc ayrıştırması geçmişte makul olabilirken bugün verimsiz kalıyor
  • Bu değişiklik JDK 26'ya (Mart 2026'da yayımlanması planlanıyor) dahil edilecek ve ThreadMXBean.getCurrentThreadUserTime() çağrılarında otomatik performans artışı sağlayacak

5 yorum

 
crawler 2026-01-15

Müthiş.

2 kat hızlandıysa akıllıca bir şey yapılmış olabilir; 100 kat hızlandıysa bu sadece aptalca bir şey yapmayı bırakmış olmak demektir

Bunun tamamen yanlış bir söz olduğunu düşünmüyorum ama işin içinde kernel varsa, yavaş olduğunu fark etmek bile gerçekten çok zor olmuştur diye düşünüyorum.

 
[Bu yorum gizlendi.]
 
princox 2026-01-19

Bunlar projelerde nasıl tespit edilebilir? Sadece yapay zeka çalıştırarak bunu anlamak zor gibi görünüyor..

Böyle örnekleri görünce ben de öğrenip bunu mutlaka bizzat deneyimlemek istiyorum.

 
aobamisaki 2026-01-15

Aslında kod tabanının tamamını baştan yazıp 2-3 kat iyileşme sağlamak bile zor bir iş; buna karşılık sadece birkaç satırı değiştirerek en fazla 400 kat hızlanma elde etmek gerçekten etkileyici.

 
GN⁺ 2026-01-15
Hacker News yorumları
  • Yazının yazarı benim. Geçen seferki kernel bug’ı yazısından sonra, JVM’in thread etkinliğini kendi içinde nasıl raporladığına baktım
    “Bu thread’in CPU kullanım süresi ne kadar?” sorusunun beklediğimden çok daha pahalı bir işlem olduğunu öğrendim
    • Nanosaniye düzeyindeki ölçümleri tartışacaksak saatin kararlılığı ve doğruluğunu çok iyi anlamak gerekir
      Atom saati seviyesinde bir referans yoksa mutlak sayılar iddia etmenin zor olduğunu düşünüyorum
    • Dağılımın neden birden çok basamak mertebesine yayıldığına bakılıp bakılmadığını merak ediyorum. Bu başlı başına ilginç bir olgu
    • Kısa TL;DR özeti için gerçekten minnettar kaldım. Bu tür özetler yazıya giriş eşiğini düşürüyor ve okuma motivasyonu yaratıyor
    • “Şaşırtıcı değil (Quelle Surprise)” diye tepki vermiş
  • clock_gettime() vDSO üzerinden çalışarak context switch’i önlüyor. Bu yüzden flamegraph’ta da izi görünüyor
    • Ama bu yalnızca bazı clock’lar için geçerli. CLOCK_VIRT ya da CLOCK_SCHED gibi durumlarda hâlâ syscall çağrısı gerekiyor
    • vDSO frame’inin altına bakınca hâlâ syscall görünüyor. Belirli clock id’ler için bir fast path uygulanmamış gibi duruyor
    • CLOCK_THREAD_CPUTIME_ID eninde sonunda kernel’e gidiyor. Çünkü task struct’a bakması gerekiyor
      İlgili kernel kaynakları: posix-cpu-timers.c,
      cputime.c,
      gettimeofday.c
  • PERF_COUNT_SW_TASK_CLOCK kullanılırsa yaklaşık 8ns seviyesinde ölçüm de yapılabiliyor
    perf_event_mmap_page üzerinden paylaşılan sayfadan okunup, delta rdtsc çağrısıyla hesaplanıyor
    Pek iyi belgelenmemiş ve açık kaynak implementasyonu da neredeyse yok
    • Gerçekten harika bir numara. Ama perf_event kurulumu ve yetki gereksinimleri yüksek olduğu için uzun ömürlü thread’ler için daha uygun görünüyor
    • seqlock neden gerekli diye sorulmuş. Sayfa değeri ile rdtsc arasında context switch olmamasını sağlamak için mi diye merak ediyor
      Muhtemelen rdtsc sonrasında sayfa değeri tekrar kontrol edilip değiştiyse yeniden deneniyor
      Bu arada clock_gettime da vdso tabanlı sanal bir syscall
    • clock_gettime bir syscall değil, vdso kullanıyor
  • Flamegraph gerçekten mükemmel bir araç
    Sadece koda bakınca her şey normal görünebiliyor ama flamegraph’a bakınca sık sık “Bu da ne?!” dedirtiyor
    Statik olmayan başlatma, tek satırlık logger çağrısının pahalı serialization tetiklemesi gibi pek çok sorun buldum
    • Ben icicle graph’ı da seviyorum. Flamegraph’ın ters yönde biriken hali; birden fazla yol ortak bir kütüphaneyi çağırdığında darboğazı görmeyi kolaylaştırıyor
    • Bu SVG örneği yeni sekmede açılırsa etkileşimli yakınlaştırma yapılabiliyor
    • Performans profilleme ve optimizasyon denemeleri, geliştirmenin en eğlenceli kısımlarından biri. Sürekli “Bu neden bu kadar yavaş?” şaşkınlığı yaşatıyor
    • String parsing ile memoization birleşiminin kulağa tuhaf geldiğini söyleyenler de vardı. Aslında sorun, pahalı regex pattern parsing işleminin cache’lenmemesiydi
    • Flamegraph’ı ilk kez kullanacak biri için temel kavramlar ve başlangıç noktası sorulmuş
  • “Görseli yeni sekmede aç” seçeneğinin gerçekten SVG etkileşimi sağlaması şaşırtıcı bulunmuş
  • OpenJDK patch’inin yazarı benim. /proc okunurken oluşan bellek ek yükünü, eBPF profillemeyi ve iyi belgelenmemiş user-space ABI’nin geçmişini ele aldım
    Ayrıntıları blog yazımda toparladım
    • Orijinal implementasyonun neden öyle yapıldığını merak eden bir soru gelmiş. Her çağrıda dosya IO ve string parsing yapmak verimsiz ama o dönemde bir nedeni olduğunu düşünüyorum
    • Jaromir yazımı görüp “Ben de aynı dönemde bir taslak yazmıştım” diyerek birbirimizin yazılarına link verdi. Benim yazımı daha titiz bulduğunu söylemesi hoşuma gitti
  • C ya da C++ gibi sistem dilleri kullanmak her zaman hızlı olacağı anlamına gelmez. Hız büyük ölçüde ne yaptığınıza bağlıdır
  • vDSO üzerinden okuma, kernel geçişi, buffer serialization ve parsing aşamalarını atladığı için çok daha hızlıdır
  • “Bir şey 2 kat hızlandıysa akıllıca bir şey yapmış olabilirsiniz; 100 kat hızlandıysa muhtemelen sadece aptalca bir şey yapmayı bırakmışsınızdır” alıntısı paylaşılmış
    Kaynak tweet
  • QuestDB ekibi bu alanda üst düzey. Hem insanlar hem de yazılım gerçekten çok iyi
    Jaromir’in blogu da gerçekten harikaydı