有 Java 编程相关的问题?

你可以在下面搜索框中键入要查询的问题!

Java8:类。getName()减慢字符串连接链的速度

最近我遇到了一个关于字符串连接的问题。本基准对其进行了总结:

@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位服务器虚拟机,25.222-b10)上,我得到了以下结果:

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

BrokenConcatenationBenchmark.slow                                    avgt   25    54,316 ±   1,340   ns/op
BrokenConcatenationBenchmark.slow:·gc.alloc.rate                     avgt   25  8435,703 ± 198,587  MB/sec
BrokenConcatenationBenchmark.slow:·gc.alloc.rate.norm                avgt   25   504,000 ±   0,001    B/op
BrokenConcatenationBenchmark.slow:·gc.churn.PS_Eden_Space            avgt   25  8434,983 ± 198,966  MB/sec
BrokenConcatenationBenchmark.slow:·gc.churn.PS_Eden_Space.norm       avgt   25   503,958 ±   1,000    B/op
BrokenConcatenationBenchmark.slow:·gc.churn.PS_Survivor_Space        avgt   25     0,127 ±   0,011  MB/sec
BrokenConcatenationBenchmark.slow:·gc.churn.PS_Survivor_Space.norm   avgt   25     0,008 ±   0,001    B/op
BrokenConcatenationBenchmark.slow:·gc.count                          avgt   25  3789,000            counts
BrokenConcatenationBenchmark.slow:·gc.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();

这里唯一可疑的是对本机方法的调用 实际上只有一次,它的结果被缓存在类的字段中。 在我的基准测试中,我将它显式地缓存在setup方法中

我希望branch predictor在每次基准测试调用时都能弄清楚这一点 这个的实际价值。name从不为null并优化整个表达式

然而,对于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的适当行为还是编译器错误

我问这个问题是因为一些项目仍在使用Java8,如果它在任何版本更新上都无法修复,那么对我来说,从热点手动取消对Class.getName()的调用是合理的

注:关于最新的JDK(11、13、14 eap),本期未转载


共 (2) 个答案

  1. # 1 楼答案

    稍微不相关,但由于Java 9和JEP 280: Indify String Concatenation,字符串连接现在使用invokedynamic而不是StringBuilder完成This article显示了Java8和Java9之间字节码的差异

    如果在较新的Java版本上重新运行的基准测试没有显示问题,那么javac中很可能没有bug,因为编译器现在使用了新的机制。如果在新版本中有如此大的变化,不确定深入研究Java8行为是否有益

  2. # 2 楼答案

    HotSpot JVM收集每个字节码的执行统计信息。如果相同的代码在不同的上下文中运行,那么结果概要文件将聚合所有上下文中的统计信息。这种效应称为profile pollution

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

    More examples of profile pollution »

    我不能称之为bug或“适当的行为”。这就是HotSpot中动态自适应编译的实现方式