Java 8: Class.getName () làm chậm chuỗi kết nối chuỗi


13

Gần đây tôi gặp phải một vấn đề liên quan đến nối chuỗi. Điểm chuẩn này tóm tắt nó:

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

Trên JDK 1.8.0_222 (OpenJDK 64-Bit Server VM, 25.222-b10) Tôi đã nhận được các kết quả sau:

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

Điều này trông giống như một vấn đề tương tự như JDK-8043677 , trong đó một biểu thức có tác dụng phụ phá vỡ tối ưu hóa StringBuilder.append().append().toString()chuỗi mới . Nhưng Class.getName()bản thân mã dường như không có bất kỳ tác dụng phụ nào:

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

Điều đáng ngờ duy nhất ở đây là một cuộc gọi đến phương thức riêng thực tế chỉ xảy ra một lần và kết quả của nó được lưu trong bộ nhớ cache của trường. Trong điểm chuẩn của tôi, tôi đã lưu trữ nó một cách rõ ràng vào phương thức thiết lập.

Tôi đã dự đoán công cụ dự đoán chi nhánh để tìm ra rằng tại mỗi lần gọi điểm chuẩn, giá trị thực của this.name không bao giờ là null và tối ưu hóa toàn bộ biểu thức.

Tuy nhiên, trong khi đối với BrokenConcatenationBenchmark.fast()tôi có điều này:

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

tức là trình biên dịch có thể nội tuyến mọi thứ, vì BrokenConcatenationBenchmark.slow()nó là khác nhau:

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

Vì vậy, câu hỏi là liệu đây là hành vi thích hợp của lỗi JVM hoặc trình biên dịch?

Tôi đang đặt câu hỏi bởi vì một số dự án vẫn đang sử dụng Java 8 và nếu nó không được sửa trong bất kỳ bản cập nhật phát hành nào thì với tôi, việc gọi các cuộc gọi đến Class.getName()các điểm nóng theo cách thủ công là hợp lý .

PS Trên các JDK mới nhất (11, 13, 14 eap), vấn đề không được sao chép.


Bạn có một tác dụng phụ ở đó - sự phân công this.name.
RealSkeptic

@RealSkeptic việc chuyển nhượng chỉ xảy ra một lần tại lần gọi đầu tiên Class.getName()và trong setUp()phương thức, không phải trong cơ thể của điểm chuẩn.
Serge Tsypanov

Câu trả lời:


7

HotSpot JVM thu thập số liệu thống kê thực hiện trên mỗi mã byte. Nếu cùng một mã được chạy trong các bối cảnh khác nhau, hồ sơ kết quả sẽ tổng hợp số liệu thống kê từ tất cả các bối cảnh. Hiệu ứng này được gọi là ô nhiễm hồ sơ .

Class.getName()rõ ràng được gọi không chỉ từ mã điểm chuẩn của bạn. Trước khi JIT bắt đầu biên dịch điểm chuẩn, nó đã biết rằng điều kiện sau Class.getName()được đáp ứng nhiều lần:

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

Ít nhất, đủ thời gian để đối xử với chi nhánh này có ý nghĩa thống kê. Vì vậy, JIT đã không loại trừ nhánh này khỏi quá trình biên dịch và do đó không thể tối ưu hóa chuỗi concat do tác dụng phụ có thể xảy ra.

Điều này thậm chí không cần phải là một cuộc gọi phương thức bản địa. Chỉ cần một sự phân công lĩnh vực thông thường cũng được coi là một tác dụng phụ.

Dưới đây là một ví dụ về cách ô nhiễm hồ sơ có thể gây hại cho việc tối ưu hóa hơn nữa.

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

Về cơ bản, đây là phiên bản sửa đổi của điểm chuẩn của bạn mô phỏng sự ô nhiễm của getName()hồ sơ. Tùy thuộc vào số lượng getName()cuộc gọi sơ bộ trên một đối tượng mới, hiệu suất nối chuỗi có thể khác nhau đáng kể:

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

Thêm ví dụ về ô nhiễm hồ sơ »

Tôi không thể gọi đó là lỗi hoặc "hành vi phù hợp". Đây chỉ là cách biên dịch thích ứng động được triển khai trong HotSpot.


1
ai khác nếu không phải Pangin ... bạn có biết Graal C2 có bị bệnh tương tự không?
Eugene

1

Hơi không liên quan nhưng vì Java 9 và JEP 280: Indify String concatenation, việc nối chuỗi bây giờ được thực hiện với invokedynamicvà không StringBuilder. Bài viết này cho thấy sự khác biệt về mã byte giữa Java 8 và Java 9.

Nếu điểm chuẩn chạy lại trên phiên bản Java mới hơn không cho thấy vấn đề thì hầu như không có lỗi nào javacvì trình biên dịch hiện sử dụng cơ chế mới. Không chắc chắn nếu đi sâu vào hành vi Java 8 có lợi hay không nếu có sự thay đổi đáng kể như vậy trong các phiên bản mới hơn.


1
Tôi đồng ý rằng đây có thể là một vấn đề biên dịch, không phải là một vấn đề liên quan đến javacmặc dù. javactạo mã byte và không thực hiện bất kỳ tối ưu hóa tinh vi nào. Tôi đã chạy cùng một điểm chuẩn với -XX:TieredStopAtLevel=1và nhận được kết quả đầu ra này: 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 Vì vậy, khi chúng tôi không tối ưu hóa nhiều cả hai phương pháp đều cho kết quả như nhau, vấn đề chỉ hiển thị khi mã được biên dịch C2.
Serge Tsypanov

1
bây giờ được thực hiện với inv invocate và không StringBuilder đơn giản là sai . invokedynamicchỉ nói với các runtime để lựa chọn như thế nào để thực hiện nối, và 5 trong 6 chiến lược (bao gồm mặc định) vẫn sử dụng StringBuilder.
Eugene

@Eugene cảm ơn bạn đã chỉ ra điều này. Khi bạn nói chiến lược, bạn có nghĩa là StringConcatFactory.Strategyenum?
Karol Dowbecki

@KarolDowbecki chính xác.
Eugene
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.