İçeriğe geç

JMH ile Doğru Ölçüm — Sayıya Güvenmeden Önce

İleri 9 dk Sık karşılaşılır

Önce şunu oku: Sınıf Yükleme ve JIT — Uygulama Neden Isınır?

30 saniyede özet

Elle yazılmış bir zamanlama döngüsü çoğu zaman ölçmek istediğin şeyi ölçmez: ısınmayı, silinmiş bir hesabı ya da bir sabiti ölçer. JMH bu tuzakları düzenler; sonucu okurken de hata payına bakılır.

Bir koşucunun hızını ölçeceksin. İlk turunu sayarsan ısınmasını ölçersin; tek yarışa bakarsan o günkü şansını.

Kodun hızını ölçmek de böyle. Üstelik Java’da koşucu bazen yolu hiç koşmadan bitiş çizgisinde belirir.

Tek bir kronometre okuması bir ölçüm değildir.
Adım adım oku
  1. Kronometre hazır, koşucu başlıyor.
  2. İlk turlar yavaş: koşucu daha ısınıyor.
  3. Birkaç turdan sonra hız oturuyor.
  4. Hakem ısınma turlarını saymıyor, yarışı birkaç kez tekrarlatıyor ve sapmayı yazıyor.
  1. Bayt: Faiz hesabını long ile yeniden yazdım ve döngüyle ölçtüm. Neredeyse sıfır sürüyor!

  2. Sen: Sıfır mı? Bu biraz fazla iyi değil mi?

  3. Bayt: Haklısın. Belki ölçtüğüm şey hesap değildi.

  4. Bayt: O zaman önce sayının neyi ölçtüğüne bakalım.

Basit bir döngü neden yanıltır?

İlki ısınma. JVM kodu önce yorumlar, sık çalışanı sonra derler; ilk çağrılar bu yüzden yavaştır. Bunu Sınıf yükleme ve JIT dersinde gördün.

Faiz hesabını bir döngüde çok kez çağırdın ama sonucu hiçbir yerde kullanmadın. Ölçülen süre neredeyse sıfır çıktı. Kod gerçekten bu kadar hızlı mı? Cevabı göster

Büyük ihtimalle hayır. Sonucu kullanılmayan bir hesap, JIT için gereksiz iştir ve silinebilir. Ölçtüğün şey boş bir döngü olabilir.

İkincisi bu: dead code eliminationDerleyicinin ya da JIT'in, sonucu hiçbir yerde kullanılmayan hesabı tamamen silmesi. Ölçümde boş bir döngünün süresini görmene yol açar.JMH'de ölçülen metot sonucunu döndürür ya da Blackhole.consume ile verir; böylece sonuç kullanılmış sayılır.Sözlükte gör →. Üçüncüsü de constant foldingGirdisi hep aynı sabit olan bir hesabın derleme anında bir kez yapılıp sonucun koda gömülmesi. Ölçülen şey hesap değil, hazır bir sabit olur.Önlemek için girdi final olmayan bir @State alanından okunur.Sözlükte gör →: girdi hep aynı sabitse, JIT hesabı önceden yapıp sonucu koda gömebilir.

Kafam karıştı, daha basit anlat

Elle ölçerken JVM’in akıllılığı sana karşı çalışır: kullanmadığın işi siler, hep aynı olan hesabı bir kez yapar.

Hızlı kontrolOrta

Bir metodu System.nanoTime ile tek bir kez çağırıp ölçtün. Bu sayı neyi ölçer?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Her ölçüm hatasını, sonucu hangi tuzağın bozduğuna göre ayır.

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Sınıflandırılmamış

Isınma

Kod henüz derlenmemiş

    Ölü kod

    Sonuç kullanılmıyor

      Sabit katlama

      Girdi hep aynı sabit

        JMH ile ölçmek

        JMH, OpenJDK’nın mikro ölçümKüçük bir kod parçasının, örneğin tek bir metodun, süresini ölçmek. JVM'de ısınma ve JIT yüzünden elle yapıldığında kolayca yanıltır.Java'da standart araç JMH'dir (Java Microbenchmark Harness). Sistemin geneli için profiler ve yük testi kullanılır.Sözlükte gör → aracıdır. Isınmayı, ayrı JVM’leri ve tekrarları senin yerine düzenler.

        Ölçülen metot sonucunu döndürür
        @State(Scope.Benchmark)
        public class InterestBenchmark {
        long balanceKurus = 1_250_000; // not final: cannot be folded
        @Benchmark
        public long dailyInterest() {
        return balanceKurus * 35 / 36_500; // returned: JMH consumes it
        }
        }

        İki kural yeter. Metot sonucunu döndürür (ya da Blackhole.consume ile verir) ve girdisini final olmayan bir @State alanından alır.

        Kafam karıştı, daha basit anlat

        Sonucu geri ver ki silinmesin. Girdiyi bir alandan al ki önceden hesaplanmasın.

        Hızlı kontrolOrta

        JMH'de ölçülen metot neden sonucunu döndürmeli?

        Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

        Ölçülecek girdi neden final olmayan bir @State alanından okunur?

        Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

        Sonucu okumak

        JMH her ölçüm için bir skor ve yanında bir hata payı yazar. İki sonucun hata payları örtüşüyorsa aralarında bir fark gösterilmiş değildir.

        Mikro ölçüm küçük bir parçayı ölçer. Bir sistemin nerede yavaşladığını ise async-profiler ya da JFR gibi bir profiler ve gerçekçi bir yük testi söyler.

        Hızlı kontrolOrta

        JMH iki yöntem için skor ve hata payı yazdı; aralıklar büyük ölçüde örtüşüyor. Ne söylersin?

        Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

        Kendin gör

        Aynı soru dört yolla ölçülüyor: faiz hesabı ne kadar sürüyor? Simülatör süre göstermiyor; her yöntemin neyi ölçtüğünü gösteriyor.

        Faiz hesabı ne kadar sürüyor? Önce neyi ölçtüğüne bak

        Tohum 1

        Oynat ya da adımla.

        Hız
        Adım 0

        Şu an ne oldu?

        Bir sayı çıkacak

        Yöntem: Elle döngü, sonuç kullanılmıyor. Her yöntem bir sayı yazdırır. Asıl soru, o sayının neyi ölçtüğü.

        Görevler0/3

        • JIT'in işi silebildiği ölçümü bulaçık

          İpucu

          Sonucu hiçbir yere gitmeyen bir hesap ara.

        • Doğru araçla bile yanlış ölçülen metodu bulaçık

          İpucu

          JMH her şeyi kurtarmaz: metodun ne döndürdüğüne bak.

        • Bütün kontrolleri geçen ölçümü kuraçık

          İpucu

          Isınma, ayrı JVM, kullanılan sonuç, değişken girdi ve hata payı.

        Olay günlüğü (0)

        Henüz olay yok. Oynat veya adımla.

        1. Varsayılanı oynat. Elle döngü: sonuç kullanılmıyor, iş silinmiş olabilir.
        2. Tek çağrıyı dene. Ölçülen şey sınıf yükleme ve yorumlayıcı.
        3. “JMH, ama metot void” seç. Doğru araç, yanlış metot.
        4. Son seçeneği oynat. Bütün kontroller geçiyor.
        Hızlı kontrolOrta

        Simülatörde JMH kullanılan bir ölçüm yine de güvenilmez çıktı. Neden?

        Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

        Tuzaklar

        @Benchmark içinde kendi döngün. JIT döngüyü açıp birleştirebilir; tekrarı JMH’ye bırak.

        Gürültülü makinede ölçmek. Tarayıcı, IDE ve derleme açıkken alınan sonuçlar birbirini tutmaz. Ölçümü sakin bir makinede, aynı koşullarda tekrarla.

        Önemsiz parçayı hızlandırmak. Profiler önce nereye bakılacağını söyler; mikro ölçüm ancak ondan sonra gelir.

        Hızlı kontrolİleri

        Bu JMH ölçümü şüpheli derecede hızlı sonuç veriyor. Sorunlu satırları işaretle.

        Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

        Hatalı satıra dokun, sonra kontrol et.

        FeeBenchmark.java
        Java 21UTF-8LF
        Derinleş · Faiz hesabı: BigDecimal ile long kuruşu adil karşılaştırmak 3 dosya · ~44 satır · ilk okumada atlayabilirsin
        Proje dosyaları

        build.gradle.kts Gradle: JMH eklentisi ölçümleri ayrı bir kaynak klasöründe tutar.

        build.gradle.kts
        plugins {
        java
        id("me.champeau.jmh") version "0.7.2"
        }
        jmh {
        warmupIterations = 5
        iterations = 10
        fork = 3
        }

        src/jmh/java/com/bank/interest/ InterestBenchmark.java Ölçüm sınıfı: iki yöntem aynı girdiyle, sonuçları döndürülerek ölçülür. Girdi deneme başında rastgele seçilir ve final değildir; JIT onu sabite çeviremez.

        src/jmh/java/com/bank/interest/InterestBenchmark.java
        @State(Scope.Benchmark)
        @BenchmarkMode(Mode.AverageTime)
        @OutputTimeUnit(TimeUnit.NANOSECONDS)
        public class InterestBenchmark {
        private static final BigDecimal RATE = new BigDecimal("0.35");
        private static final BigDecimal DAYS = new BigDecimal("365");
        private BigDecimal balance;
        private long balanceKurus;
        @Setup
        public void setUp() {
        long kurus = ThreadLocalRandom.current().nextLong(1_000, 100_000_000);
        balanceKurus = kurus;
        balance = BigDecimal.valueOf(kurus, 2);
        }
        @Benchmark
        public BigDecimal withBigDecimal() {
        return balance.multiply(RATE).divide(DAYS, 2, RoundingMode.HALF_EVEN);
        }
        @Benchmark
        public long withKurus() {
        // Integer arithmetic truncates: the rounding rule is now this method's job.
        return balanceKurus * 35 / 36_500;
        }
        }

        ops/ run-benchmarks.sh Çalıştırma: ısınma, ölçüm ve fork sayıları Gradle'da açıkça verilir; sonuçlar build klasörüne yazılır.

        ops/run-benchmarks.sh
        #!/usr/bin/env bash
        # Run on a quiet machine; compare scores only together with their error margins.
        set -euo pipefail
        ./gradlew jmh
        ls build/results/jmh/

        Kendini sına

        Önce hızlı bir ısınma: puan yok, kayıt yok. Sonra asıl sorular.

        Şimşek turu1/4

        Sonucu kullanılmayan bir hesap JIT tarafından silinebilir.

        Soru 1/3İleri

        Bir servis yavaş ve ekip hemen JMH ile bir metodu hızlandırmak istiyor. İlk adım ne olmalı?

        Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

        Aklında kalacak üç şey

        1. 1 Elle yazılmış bir zamanlama döngüsü ısınmayı, JIT'in sildiği bir hesabı ya da önceden hesaplanmış bir sabiti ölçebilir. Yanlış ölçüm hata vermez, yanlış bir sayı verir.
        2. 2 JMH ısınma turlarını ayırır, her denemeyi ayrı bir JVM'de koşturur ve hata payı verir. Ölçülen metot sonucunu döndürür, girdisini de @State'ten alır.
        3. 3 Hata payları örtüşen iki sonuç arasında fark yoktur. Mikro ölçüm bir parçayı ölçer; sistemin nerede yavaşladığını profiler ve yük testi söyler.
        Sonraki kapı Ödeme servisinin CPU grafiği limitin yüzde otuzunda duruyor. Yine de bazı istekler üç kat yavaş. CPU boştaysa bu süre nereye gidiyor? JVM Konteynerde CPU — Ortalama Düşük, İstek Neden Yavaş? · 8 dk

        4 kart sonraki derste seni bekliyor

        0/4 kart bu dersten toplandı