Java 8: Class.getName (), String birleştirme zincirini yavaşlatır


13

Son zamanlarda Dize birleştirme ile ilgili bir sorunla karşılaştım. Bu temel ölçü şöyle özetliyor:

@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class BrokenConcatenationBenchmark {

  @Benchmark
  public String slow(Data data) {
    final Class<? extends Data> clazz = data.clazz;
    return "class " + clazz.getName();
  }

  @Benchmark
  public String fast(Data data) {
    final Class<? extends Data> clazz = data.clazz;
    final String clazzName = clazz.getName();
    return "class " + clazzName;
  }

  @State(Scope.Thread)
  public static class Data {
    final Class<? extends Data> clazz = getClass();

    @Setup
    public void setup() {
      //explicitly load name via native method Class.getName0()
      clazz.getName();
    }
  }
}

JDK 1.8.0_222'de (OpenJDK 64-Bit Sunucu VM, 25.222-b10) Aşağıdaki sonuçları aldım:

Benchmark                                                            Mode  Cnt     Score     Error   Units
BrokenConcatenationBenchmark.fast                                    avgt   25    22,253 ±   0,962   ns/op
BrokenConcatenationBenchmark.fastgc.alloc.rate                     avgt   25  9824,603 ± 400,088  MB/sec
BrokenConcatenationBenchmark.fastgc.alloc.rate.norm                avgt   25   240,000 ±   0,001    B/op
BrokenConcatenationBenchmark.fastgc.churn.PS_Eden_Space            avgt   25  9824,162 ± 397,745  MB/sec
BrokenConcatenationBenchmark.fastgc.churn.PS_Eden_Space.norm       avgt   25   239,994 ±   0,522    B/op
BrokenConcatenationBenchmark.fastgc.churn.PS_Survivor_Space        avgt   25     0,040 ±   0,011  MB/sec
BrokenConcatenationBenchmark.fastgc.churn.PS_Survivor_Space.norm   avgt   25     0,001 ±   0,001    B/op
BrokenConcatenationBenchmark.fastgc.count                          avgt   25  3798,000            counts
BrokenConcatenationBenchmark.fastgc.time                           avgt   25  2241,000                ms

BrokenConcatenationBenchmark.slow                                    avgt   25    54,316 ±   1,340   ns/op
BrokenConcatenationBenchmark.slowgc.alloc.rate                     avgt   25  8435,703 ± 198,587  MB/sec
BrokenConcatenationBenchmark.slowgc.alloc.rate.norm                avgt   25   504,000 ±   0,001    B/op
BrokenConcatenationBenchmark.slowgc.churn.PS_Eden_Space            avgt   25  8434,983 ± 198,966  MB/sec
BrokenConcatenationBenchmark.slowgc.churn.PS_Eden_Space.norm       avgt   25   503,958 ±   1,000    B/op
BrokenConcatenationBenchmark.slowgc.churn.PS_Survivor_Space        avgt   25     0,127 ±   0,011  MB/sec
BrokenConcatenationBenchmark.slowgc.churn.PS_Survivor_Space.norm   avgt   25     0,008 ±   0,001    B/op
BrokenConcatenationBenchmark.slowgc.count                          avgt   25  3789,000            counts
BrokenConcatenationBenchmark.slowgc.time                           avgt   25  2245,000                ms

Bu , yan etkisi olan bir ifadenin yeni zincirin optimizasyonunu bozduğu JDK-8043677'ye benzer bir soruna benziyor StringBuilder.append().append().toString(). Ancak kodun Class.getName()kendisinin herhangi bir yan etkisi yok gibi görünüyor:

private transient String name;

public String getName() {
  String name = this.name;
  if (name == null) {
    this.name = name = this.getName0();
  }

  return name;
}

private native String getName0();

Buradaki tek şüpheli şey, aslında sadece bir kez gerçekleşen yerel bir çağrıdır ve sonucu sınıf alanında önbelleğe alınır. Kıyaslamamda, kurulum yönteminde açıkça önbelleğe aldım.

Şube tahmincisinin her bir karşılaştırmalı değerlendirme çağrısında this.name'nin gerçek değerinin asla boş olmadığını ve tüm ifadeyi optimize ettiğini tahmin ettim.

Ancak, süre için BrokenConcatenationBenchmark.fast()ben var:

@ 19   tsypanov.strings.benchmark.concatenation.BrokenConcatenationBenchmark::fast (30 bytes)   force inline by CompileCommand
  @ 6   java.lang.Class::getName (18 bytes)   inline (hot)
    @ 14   java.lang.Class::initClassName (0 bytes)   native method
  @ 14   java.lang.StringBuilder::<init> (7 bytes)   inline (hot)
  @ 19   java.lang.StringBuilder::append (8 bytes)   inline (hot)
  @ 23   java.lang.StringBuilder::append (8 bytes)   inline (hot)
  @ 26   java.lang.StringBuilder::toString (35 bytes)   inline (hot)

yani derleyici her şeyi satır içine alabilir, çünkü BrokenConcatenationBenchmark.slow()farklıdır:

@ 19   tsypanov.strings.benchmark.concatenation.BrokenConcatenationBenchmark::slow (28 bytes)   force inline by CompilerOracle
  @ 9   java.lang.StringBuilder::<init> (7 bytes)   inline (hot)
    @ 3   java.lang.AbstractStringBuilder::<init> (12 bytes)   inline (hot)
      @ 1   java.lang.Object::<init> (1 bytes)   inline (hot)
  @ 14   java.lang.StringBuilder::append (8 bytes)   inline (hot)
    @ 2   java.lang.AbstractStringBuilder::append (50 bytes)   inline (hot)
      @ 10   java.lang.String::length (6 bytes)   inline (hot)
      @ 21   java.lang.AbstractStringBuilder::ensureCapacityInternal (27 bytes)   inline (hot)
        @ 17   java.lang.AbstractStringBuilder::newCapacity (39 bytes)   inline (hot)
        @ 20   java.util.Arrays::copyOf (19 bytes)   inline (hot)
          @ 11   java.lang.Math::min (11 bytes)   (intrinsic)
          @ 14   java.lang.System::arraycopy (0 bytes)   (intrinsic)
      @ 35   java.lang.String::getChars (62 bytes)   inline (hot)
        @ 58   java.lang.System::arraycopy (0 bytes)   (intrinsic)
  @ 18   java.lang.Class::getName (21 bytes)   inline (hot)
    @ 11   java.lang.Class::getName0 (0 bytes)   native method
  @ 21   java.lang.StringBuilder::append (8 bytes)   inline (hot)
    @ 2   java.lang.AbstractStringBuilder::append (50 bytes)   inline (hot)
      @ 10   java.lang.String::length (6 bytes)   inline (hot)
      @ 21   java.lang.AbstractStringBuilder::ensureCapacityInternal (27 bytes)   inline (hot)
        @ 17   java.lang.AbstractStringBuilder::newCapacity (39 bytes)   inline (hot)
        @ 20   java.util.Arrays::copyOf (19 bytes)   inline (hot)
          @ 11   java.lang.Math::min (11 bytes)   (intrinsic)
          @ 14   java.lang.System::arraycopy (0 bytes)   (intrinsic)
      @ 35   java.lang.String::getChars (62 bytes)   inline (hot)
        @ 58   java.lang.System::arraycopy (0 bytes)   (intrinsic)
  @ 24   java.lang.StringBuilder::toString (17 bytes)   inline (hot)

Yani soru bu JVM veya derleyici hata uygun davranış olup olmadığıdır?

Soruyu soruyorum çünkü bazı projeler hala Java 8 kullanıyor ve sürüm güncellemelerinden herhangi birine sabitlenmeyecekse, aramaları Class.getName()sıcak noktalardan manuel olarak kaldırmak mantıklı .

PS En son JDK'larda (11, 13, 14-eap) sorun yeniden oluşturulmaz.


Orada bir yan etkiniz var - atama this.name.
RealSkeptic

@RealSkeptic, ödev , kıyaslananın gövdesinde değil, ilk çağrılmada Class.getName()ve setUp()yöntemde yalnızca bir kez gerçekleşir .
Sergey Tsypanov

Yanıtlar:


7

HotSpot JVM, bayt kodu başına yürütme istatistiklerini toplar. Aynı kod farklı bağlamlarda çalıştırılırsa, sonuç profili tüm bağlamlardan istatistikleri toplar. Bu etki profil kirliliği olarak bilinir .

Class.getName()Açıkçası sadece karşılaştırma kodunuzdan değil. JIT karşılaştırmayı derlemeye başlamadan önce, aşağıdaki koşulun Class.getName()birden çok kez karşılandığını zaten biliyor :

    if (name == null)
        this.name = name = getName0();

En azından, bu dalı tedavi etmek için yeterli zaman istatistiksel olarak önemlidir. Bu nedenle, JIT bu dalı derlemeden hariç tutmadı ve bu nedenle olası yan etki nedeniyle dize kesişmesini optimize edemedi.

Bunun yerel bir yöntem çağrısı olması bile gerekmez. Sadece normal bir saha ataması da bir yan etki olarak kabul edilir.

Profil kirliliğinin daha fazla optimizasyona nasıl zarar verebileceğini gösteren bir örnek.

@State(Scope.Benchmark)
public class StringConcat {
    private final MyClass clazz = new MyClass();

    static class MyClass {
        private String name;

        public String getName() {
            if (name == null) name = "ZZZ";
            return name;
        }
    }

    @Param({"1", "100", "400", "1000"})
    private int pollutionCalls;

    @Setup
    public void setup() {
        for (int i = 0; i < pollutionCalls; i++) {
            new MyClass().getName();
        }
    }

    @Benchmark
    public String fast() {
        String clazzName = clazz.getName();
        return "str " + clazzName;
    }

    @Benchmark
    public String slow() {
        return "str " + clazz.getName();
    }
}

Bu temelde, karşılaştırmalı değerlendirmenin getName()profil kirliliğini simüle eden değiştirilmiş sürümüdür . getName()Yeni bir nesne üzerindeki ön çağrıların sayısına bağlı olarak, dize birleştirme işleminin diğer performansı önemli ölçüde farklılık gösterebilir:

Benchmark          (pollutionCalls)  Mode  Cnt   Score   Error  Units
StringConcat.fast                 1  avgt   15  11,458 ± 0,076  ns/op
StringConcat.fast               100  avgt   15  11,690 ± 0,222  ns/op
StringConcat.fast               400  avgt   15  12,131 ± 0,105  ns/op
StringConcat.fast              1000  avgt   15  12,194 ± 0,069  ns/op
StringConcat.slow                 1  avgt   15  11,771 ± 0,105  ns/op
StringConcat.slow               100  avgt   15  11,963 ± 0,212  ns/op
StringConcat.slow               400  avgt   15  26,104 ± 0,202  ns/op  << !
StringConcat.slow              1000  avgt   15  26,108 ± 0,436  ns/op  << !

Profil kirliliğinin diğer örnekleri »

Ben bir hata ya da "uygun davranış" diyemem. HotSpot'ta dinamik uyarlamalı derleme böyle yapılır.


1
Pangin değilse başka kim ... Graal C2'nin aynı hastalığa sahip olup olmadığını biliyor musun?
Eugene

1

Biraz ilgisiz ancak Java 9 ve JEP 280: Dize Birleştirmesini Belirtin dize birleştirmesi şimdi ile yapılır invokedynamicve yapılmaz StringBuilder. Bu makalede , Java 8 ve Java 9 arasındaki bayt kodundaki farklar gösterilmektedir.

Daha yeni Java sürümünde karşılaştırmalı değerlendirme sorunu göstermezse javac, derleyici artık yeni bir mekanizma kullandığından , çoğu hata yoktur . Daha yeni sürümlerde önemli bir değişiklik varsa Java 8 davranışına dalmanın faydalı olup olmadığından emin değilim.


1
Bunun bir derleyici sorunu olacağını kabul ediyorum, bununla ilgili bir sorun değil javac. javacbayt kodu oluşturur ve herhangi bir gelişmiş optimizasyon yapmaz. Ben aynı ölçüt ile çalıştırmak -XX:TieredStopAtLevel=1ve bu çıktı aldım : Benchmark Mode Cnt Score Error Units BrokenConcatenationBenchmark.fast avgt 25 74,677 ? 2,961 ns/op BrokenConcatenationBenchmark.slow avgt 25 69,316 ? 1,239 ns/op Yani biz her iki yöntem de pek çok optimizasyon olmadığında aynı sonuçları verir, sorun sadece kod C2 derlendi alır zaman kendini ortaya koymaktadır.
Sergey Tsypanov

1
şimdi invokedynamic ile yapılır ve StringBuilder değil sadece yanlış . çalışma zamanınainvokedynamic yalnızca birleştirmenin nasıl yapılacağını seçmesini söyler ve 6 stratejiden 5'i (varsayılan dahil) kullanılmaya devam eder . StringBuilder
Eugene

@Eugene bunu işaret ettiğiniz için teşekkürler. Strateji derken StringConcatFactory.Strategyenum demek mi?
Karol Dowbecki

@KarolDowbecki tam olarak.
Eugene
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.