जावा 8: Class.getName () धीमा हो जाता है स्ट्रिंग संघनन श्रृंखला


13

हाल ही में मैं स्ट्रिंग मुद्दे पर एक मुद्दे पर चला हूं। यह मानदंड इसे सारांशित करता है:

@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 (OpenJDK 64-बिट सर्वर VM, 25.222-b10) पर मुझे निम्नलिखित परिणाम मिले हैं:

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

यह JDK-8043677 के समान एक समस्या की तरह दिखता है , जहां एक साइड इफेक्ट होने से नई StringBuilder.append().append().toString()श्रृंखला का अनुकूलन टूट जाता है । लेकिन Class.getName()खुद के कोड का कोई साइड इफेक्ट नहीं लगता है:

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();

यहाँ एकमात्र संदेहास्पद बात देशी पद्धति की है जो वास्तव में केवल एक बार होती है और इसका परिणाम वर्ग के क्षेत्र में होता है। मेरे बेंचमार्क में मैंने इसे सेटअप विधि में स्पष्ट रूप से कैश किया है।

मुझे उम्मीद है कि शाखा भविष्यवक्ता यह पता लगाने के लिए कि प्रत्येक बेंचमार्क आह्वान पर इस का वास्तविक मूल्य कभी भी शून्य नहीं है और पूरी अभिव्यक्ति को अनुकूलित करता है।

हालाँकि, जबकि BrokenConcatenationBenchmark.fast()मेरे पास यह है:

@ 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)

यानी कंपाइलर सब कुछ इनलाइन करने में सक्षम है, क्योंकि BrokenConcatenationBenchmark.slow()यह अलग है:

@ 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)

तो सवाल यह है कि क्या यह जेवीएम या कंपाइलर बग का उचित व्यवहार है?

मैं सवाल पूछ रहा हूं क्योंकि कुछ परियोजनाएं अभी भी जावा 8 का उपयोग कर रही हैं और अगर यह किसी भी अपडेट अपडेट पर तय नहीं होगी, तो मेरे लिए यह Class.getName()हॉट स्पॉट से मैन्युअल रूप से कॉल को उठाना उचित है ।

PS नवीनतम JDKs (11, 13, 14-eap) पर इस मुद्दे को पुन: पेश नहीं किया जाता है।


आप वहाँ एक पक्ष प्रभाव है - करने के लिए काम this.name
रियलस्केप्टिक

@RealSkeptic असाइनमेंट केवल एक बार पहले तरीके से Class.getName()और setUp()विधि से होता है, बेंचमार्क वाले के शरीर में नहीं।
सर्गेई त्सिपानोव

जवाबों:


7

हॉटस्पॉट जेवीएम प्रति बायोटेक के निष्पादन के आंकड़े एकत्र करता है। यदि समान कोड अलग-अलग संदर्भों में चलाया जाता है, तो परिणाम प्रोफ़ाइल सभी संदर्भों से आँकड़ों को एकत्र करेगा। इस प्रभाव को प्रोफ़ाइल प्रदूषण के रूप में जाना जाता है

Class.getName()स्पष्ट रूप से न केवल आपके बेंचमार्क कोड से कहा जाता है। JIT बेंचमार्क संकलित करना शुरू करने से पहले, यह पहले से ही जानता है कि निम्न स्थिति Class.getName()कई बार मिली थी:

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

कम से कम, इस शाखा को सांख्यिकीय रूप से महत्वपूर्ण मानने के लिए पर्याप्त समय। इसलिए, JIT ने इस शाखा को संकलन से बाहर नहीं किया, और इस प्रकार संभावित दुष्प्रभावों के कारण स्ट्रिंग कॉनैट का अनुकूलन नहीं कर सका।

इसके लिए देशी विधि कॉल करने की भी आवश्यकता नहीं है। बस एक नियमित क्षेत्र असाइनमेंट को एक साइड इफेक्ट भी माना जाता है।

यहाँ एक उदाहरण है कि कैसे प्रोफ़ाइल प्रदूषण आगे अनुकूलन को नुकसान पहुंचा सकता है।

@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();
    }
}

यह मूल रूप से आपके बेंचमार्क का संशोधित संस्करण है जो getName()प्रोफ़ाइल के प्रदूषण का अनुकरण करता है । getName()एक ताजा वस्तु पर प्रारंभिक कॉल की संख्या के आधार पर , स्ट्रिंग संघनन का आगे का प्रदर्शन नाटकीय रूप से भिन्न हो सकता है:

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  << !

प्रोफ़ाइल प्रदूषण के और अधिक उदाहरण »

मैं इसे बग या "उपयुक्त व्यवहार" नहीं कह सकता। यह सिर्फ हॉटस्पॉट में गतिशील अनुकूली संकलन कैसे लागू किया जाता है।


1
अगर पैंगिन नहीं तो और कौन है ... क्या आपको पता है कि ग्रेगल सी 2 में भी यही बीमारी है?
यूजीन

1

थोड़ा असंबंधित लेकिन जावा 9 और जेईपी 280 के बाद से : स्ट्रींग कॉनटेनैशन को इंडिफाई करें स्ट्रिंग स्ट्रिंग को अब किया जाता है invokedynamicऔर नहीं StringBuilderयह आलेख जावा 8 और जावा 9 के बीच बायटेकोड में अंतर को दर्शाता है।

यदि नए जावा संस्करण पर बेंचमार्क फिर से चलता है, तो समस्या यह नहीं दिखाती है कि सबसे अधिक कोई बग नहीं है javacक्योंकि कंपाइलर अब नए तंत्र का उपयोग करता है। सुनिश्चित नहीं है कि जावा 8 व्यवहार में गोताखोरी फायदेमंद है अगर नए संस्करणों में इस तरह के महत्वपूर्ण बदलाव हैं।


1
मैं मानता हूं कि यह एक संकलक मुद्दा होने की संभावना है, javacहालांकि किसी से संबंधित नहीं है । javacबायटेकोड उत्पन्न करता है और कोई परिष्कृत अनुकूलन नहीं करता है। मैंने एक ही बेंचमार्क चलाया है -XX:TieredStopAtLevel=1और इस आउटपुट को प्राप्त किया है: 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 इसलिए जब हम दोनों तरीकों को समान परिणाम नहीं देते हैं, तो समस्या का खुलासा केवल तभी होता है जब कोड C2- संकलित हो जाता है।
सर्गेई त्सिपानोव

1
अब इनवॉकेन्जेनिक के साथ किया जाता है न कि स्ट्रिंगब्यूलर बस गलत हैinvokedynamicकेवल रनटाइम बताता है कि कॉन्टेक्ट कैसे करना है, और 6 में से 5 रणनीतियों (डिफ़ॉल्ट सहित) अभी भी उपयोग करते हैं StringBuilder
यूजीन

@ यूजीन ने इसे इंगित करने के लिए धन्यवाद दिया। जब आप कहते हैं कि रणनीतियों का मतलब है StringConcatFactory.Strategy?
करोल दोबेकी

@KarolDowbecki बिल्कुल।
यूजीन
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.