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

ดังนั้นคำถามคือว่านี่เป็นพฤติกรรมที่เหมาะสมของ JVM หรือคอมไพเลอร์บั๊กหรือไม่?

ฉันถามคำถามเพราะบางโครงการยังคงใช้ Java 8 อยู่และถ้ามันไม่ได้รับการแก้ไขในการอัพเดทใด ๆ กับฉันมันสมเหตุสมผลที่จะยกการโทรClass.getName()ด้วยตนเองจากฮอตสปอต

PS บน JDK ล่าสุด (11, 13, 14-eap) ปัญหาไม่ได้ถูกทำซ้ำ


คุณจะมีผลข้างเคียงมี - this.nameการกำหนดไป
RealSkeptic

@ RealSkeptic การมอบหมายเกิดขึ้นเพียงครั้งเดียวที่การร้องขอครั้งแรกClass.getName()และในsetUp()วิธีการไม่ได้อยู่ในร่างของเกณฑ์มาตรฐาน
Sergey Tsypanov

คำตอบ:


7

HotSpot JVM รวบรวมสถิติการดำเนินการต่อไบต์ หากมีการเรียกใช้รหัสเดียวกันในบริบทที่แตกต่างกันโปรไฟล์ผลลัพธ์จะรวมสถิติจากบริบททั้งหมด ผลกระทบนี้จะเป็นที่รู้จักกันมลพิษรายละเอียด

Class.getName()เห็นได้ชัดว่าเรียกไม่เพียง แต่จากรหัสมาตรฐานของคุณ ก่อนที่ JIT จะเริ่มทำการรวบรวมเกณฑ์มาตรฐานมันก็รู้อยู่แล้วว่าเงื่อนไขต่อไปนี้Class.getName()ได้รับการตอบสนองหลายครั้ง:

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

อย่างน้อยก็มีเวลาพอที่จะปฏิบัติต่อสาขานี้สำคัญทางสถิติ ดังนั้น JIT จึงไม่ได้แยกสาขานี้ออกจากการรวบรวมและทำให้ไม่สามารถปรับสตริง concat ให้เหมาะสมเนื่องจากผลข้างเคียงที่อาจเกิดขึ้นได้

สิ่งนี้ไม่จำเป็นต้องเป็นการเรียกใช้เมธอดดั้งเดิม เพียงแค่การมอบหมายฟิลด์ปกติก็ถือว่าเป็นผลข้างเคียง

นี่คือตัวอย่างว่ามลพิษทางโปรไฟล์สามารถเป็นอันตรายต่อการเพิ่มประสิทธิภาพได้อย่างไร

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

ตัวอย่างเพิ่มเติมของรายละเอียดมลพิษ»

ฉันไม่สามารถเรียกมันว่าเป็นบั๊กหรือ "พฤติกรรมที่เหมาะสม" นี่เป็นเพียงวิธีการรวบรวมการปรับตัวแบบไดนามิกใน HotSpot


1
มีใครอีกถ้าไม่ใช่ Pangin ... คุณรู้หรือไม่ว่า Graal C2 มีอาการป่วยเหมือนกันหรือไม่?
Eugene

1

เล็กน้อยที่ไม่เกี่ยวข้องกัน แต่เนื่องจาก Java 9 และJEP 280: Indify สตริง concatenation สตริงจะทำตอนนี้มีไม่invokedynamic บทความนี้แสดงความแตกต่างใน bytecode ระหว่าง Java 8 และ Java 9StringBuilder

หากเบนช์มาร์กรันใหม่ในเวอร์ชั่น Java รุ่นใหม่จะไม่แสดงปัญหา แต่อย่างใดjavacเพราะส่วนใหญ่คอมไพเลอร์ใช้กลไกใหม่ ไม่แน่ใจว่าการดำน้ำในพฤติกรรม Java 8 มีประโยชน์หรือไม่หากมีการเปลี่ยนแปลงที่สำคัญในเวอร์ชันที่ใหม่กว่านี้


1
ผมเห็นด้วยนี้มีแนวโน้มที่จะเป็นปัญหาคอมไพเลอร์ไม่ได้เป็นหนึ่งที่เกี่ยวข้องกับjavacแม้ว่า javacสร้าง bytecode และไม่ทำการปรับแต่งที่ซับซ้อนใด ๆ ฉันใช้เกณฑ์มาตรฐานเดียวกันกับ-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
Sergey Tsypanov

1
จะทำตอนนี้มี invokedynamic และไม่ StringBuilderเป็นเพียงที่ไม่ถูกต้อง invokedynamicเพียงบอกรันไทม์ที่จะเลือกวิธีที่จะทำเรียงต่อกันและ 5 จาก 6 กลยุทธ์ (รวมถึงการเริ่มต้น) StringBuilderยังคงใช้
Eugene

@Eugene ขอบคุณที่ชี้ให้เห็น เมื่อคุณพูดว่ากลยุทธ์หมายถึงStringConcatFactory.Strategyenum?
Karol Dowbecki

@ KarolDowbecki อย่างแน่นอน
ยูจีน
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.