programing

Java 7에서 Java 8보다 StringBuilder # append (int)가 더 빠른 이유는 무엇입니까?

nasanasas 2020. 10. 17. 10:36
반응형

Java 7에서 Java 8보다 StringBuilder # append (int)가 더 빠른 이유는 무엇입니까?


wrt를 사용 하고 정수 프리미티브를 문자열로 변환 하기 위해 약간의 논쟁을 조사하는 동안 JMH 마이크로 벤치 마크를 작성했습니다 ."" + nInteger.toString(int)

@Fork(1)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@State(Scope.Benchmark)
public class IntStr {
    protected int counter;


    @GenerateMicroBenchmark
    public String integerToString() {
        return Integer.toString(this.counter++);
    }

    @GenerateMicroBenchmark
    public String stringBuilder0() {
        return new StringBuilder().append(this.counter++).toString();
    }

    @GenerateMicroBenchmark
    public String stringBuilder1() {
        return new StringBuilder().append("").append(this.counter++).toString();
    }

    @GenerateMicroBenchmark
    public String stringBuilder2() {
        return new StringBuilder().append("").append(Integer.toString(this.counter++)).toString();
    }

    @GenerateMicroBenchmark
    public String stringFormat() {
        return String.format("%d", this.counter++);
    }

    @Setup(Level.Iteration)
    public void prepareIteration() {
        this.counter = 0;
    }
}

내 Linux 컴퓨터에있는 두 Java VM (최신 Mageia 4 64 비트, Intel i7-3770 CPU, 32GB RAM)에서 기본 JMH 옵션으로 실행했습니다. 첫 번째 JVM은 Oracle JDK 8u5 64 비트와 함께 제공된 것입니다.

java version "1.8.0_05"
Java(TM) SE Runtime Environment (build 1.8.0_05-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.5-b02, mixed mode)

이 JVM을 사용하여 예상했던 것을 거의 얻었습니다.

Benchmark                    Mode   Samples         Mean   Mean error    Units
b.IntStr.integerToString    thrpt        20    32317.048      698.703   ops/ms
b.IntStr.stringBuilder0     thrpt        20    28129.499      421.520   ops/ms
b.IntStr.stringBuilder1     thrpt        20    28106.692     1117.958   ops/ms
b.IntStr.stringBuilder2     thrpt        20    20066.939     1052.937   ops/ms
b.IntStr.stringFormat       thrpt        20     2346.452       37.422   ops/ms

StringBuilder, StringBuilder개체 를 만들고 빈 문자열을 추가하는 추가 오버 헤드로 인해 클래스를 사용하는 속도가 느립니다 . 사용 String.format(String, ...)은 훨씬 느립니다.

반면 배포판 제공 컴파일러는 OpenJDK 1.7을 기반으로합니다.

java version "1.7.0_55"
OpenJDK Runtime Environment (mageia-2.4.7.1.mga4-x86_64 u55-b13)
OpenJDK 64-Bit Server VM (build 24.51-b03, mixed mode)

여기의 결과는 흥미웠다 .

Benchmark                    Mode   Samples         Mean   Mean error    Units
b.IntStr.integerToString    thrpt        20    31249.306      881.125   ops/ms
b.IntStr.stringBuilder0     thrpt        20    39486.857      663.766   ops/ms
b.IntStr.stringBuilder1     thrpt        20    41072.058      484.353   ops/ms
b.IntStr.stringBuilder2     thrpt        20    20513.913      466.130   ops/ms
b.IntStr.stringFormat       thrpt        20     2068.471       44.964   ops/ms

StringBuilder.append(int)이 JVM에서 그렇게 더 빨리 나타 납니까? StringBuilder클래스 소스 코드를 살펴보면 특별히 흥미로운 것은 없습니다 Integer#toString(int). 문제의 방법은 . 흥미롭게도 Integer.toString(int)( stringBuilder2마이크로 벤치 마크) 의 결과를 추가하는 것이 더 빠르지 않은 것 같습니다.

이 성능 불일치는 테스트 장치의 문제입니까? 아니면 내 OpenJDK JVM에이 특정 코드 (안티) 패턴에 영향을주는 최적화가 포함되어 있습니까?

편집하다:

보다 직접적인 비교를 위해 Oracle JDK 1.7u55를 설치했습니다.

java version "1.7.0_55"
Java(TM) SE Runtime Environment (build 1.7.0_55-b13)
Java HotSpot(TM) 64-Bit Server VM (build 24.55-b03, mixed mode)

결과는 OpenJDK의 결과와 유사합니다.

Benchmark                    Mode   Samples         Mean   Mean error    Units
b.IntStr.integerToString    thrpt        20    32502.493      501.928   ops/ms
b.IntStr.stringBuilder0     thrpt        20    39592.174      428.967   ops/ms
b.IntStr.stringBuilder1     thrpt        20    40978.633      544.236   ops/ms

이것은보다 일반적인 Java 7 대 Java 8 문제인 것 같습니다. 아마도 Java 7에는 더 적극적인 문자열 최적화가 있었습니까?

편집 2 :

완전성을 위해 다음은 두 JVM에 대한 문자열 관련 VM 옵션입니다.

Oracle JDK 8u5의 경우 :

$ /usr/java/default/bin/java -XX:+PrintFlagsFinal 2>/dev/null | grep String
     bool OptimizeStringConcat                      = true            {C2 product}
     intx PerfMaxStringConstLength                  = 1024            {product}
     bool PrintStringTableStatistics                = false           {product}
    uintx StringTableSize                           = 60013           {product}

OpenJDK 1.7의 경우 :

$ java -XX:+PrintFlagsFinal 2>/dev/null | grep String
     bool OptimizeStringConcat                      = true            {C2 product}        
     intx PerfMaxStringConstLength                  = 1024            {product}           
     bool PrintStringTableStatistics                = false           {product}           
    uintx StringTableSize                           = 60013           {product}           
     bool UseStringCache                            = false           {product}   

UseStringCache옵션은 대체하지 않고 Java 8에서 제거되었으므로 차이가 있는지 의심됩니다. 나머지 옵션은 동일한 설정으로 표시됩니다.

편집 3 :

소스의 코드의 나란히 비교 AbstractStringBuilder, StringBuilder그리고 Integer로부터 클래스 src.zip의 파일은 아무것도 noteworty을 보여준다. 많은 외관 및 문서 변경 사항 외에도 Integer부호없는 정수에 대한 일부 지원 StringBuilder이 있으며 더 많은 코드를 StringBuffer. 이 변경 사항 중 어느 것도에서 사용하는 코드 경로에 영향을 미치지 않는 것 같습니다 StringBuilder#append(int).

에 대해 생성 된 어셈블리 코드의 비교 IntStr#integerToString()IntStr#stringBuilder0()훨씬 더 재미있다. 생성 된 코드의 기본 레이아웃은 IntStr#integerToString()두 JVM 모두에서 비슷했지만, Oracle JDK 8u5는 Integer#toString(int)코드 내에서 일부 호출을 인라인하는보다 공격적인 wrt로 보였습니다 . 최소한의 어셈블리 경험을 가진 사람에게도 Java 소스 코드와 명확한 일치가있었습니다.

IntStr#stringBuilder0()그러나 의 어셈블리 코드 는 근본적으로 달랐습니다. Oracle JDK 8u5에서 생성 된 코드는 다시 한 번 Java 소스 코드와 직접 관련이 있습니다. 동일한 레이아웃을 쉽게 인식 할 수있었습니다. 반대로 OpenJDK 7에 의해 생성 된 코드는 훈련되지 않은 눈 (예 : 내 눈)에게는 거의 인식 할 수 없었습니다. new StringBuilder()에 배열 생성 있다는 콜 겉보기 제거 하였다 StringBuilder생성자. 추가로, 디스어셈블러 플러그인은 JDK 8 에서처럼 소스 코드에 대한 많은 참조를 제공 할 수 없었습니다.

이것은 OpenJDK 7에서 훨씬 더 공격적인 최적화 패스의 결과이거나 특정 StringBuilder작업에 대해 손으로 작성한 저수준 코드를 삽입 한 결과라고 가정 합니다. 이 최적화가 내 JVM 8 구현에서 발생하지 않는 이유 또는 동일한 최적화가 Integer#toString(int)JVM 7에서 구현되지 않은 이유를 잘 모르겠습니다. JRE 소스 코드의 관련 부분에 익숙한 사람이 이러한 질문에 답해야 할 것 같습니다.


TL; DR : 부작용으로 인해 appendStringConcat 최적화가 중단되었습니다.

원래 질문과 업데이트에서 아주 좋은 분석!

완전성을 위해 다음은 몇 가지 누락 된 단계입니다.

  • -XX:+PrintInlining7u55 및 8u5 모두 를 통해 보십시오. 7u55에서는 다음과 같은 내용이 표시됩니다.

     @ 16   org.sample.IntStr::inlineSideEffect (25 bytes)   force inline by CompilerOracle
       @ 4   java.lang.StringBuilder::<init> (7 bytes)   inline (hot)
       @ 18   java.lang.StringBuilder::append (8 bytes)   already compiled into a big method
       @ 21   java.lang.StringBuilder::toString (17 bytes)   inline (hot)
    

    ... 그리고 8u5에서 :

     @ 16   org.sample.IntStr::inlineSideEffect (25 bytes)   force inline by CompilerOracle
       @ 4   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)
       @ 18   java.lang.StringBuilder::append (8 bytes)   inline (hot)
         @ 2   java.lang.AbstractStringBuilder::append (62 bytes)   already compiled into a big method
       @ 21   java.lang.StringBuilder::toString (17 bytes)   inline (hot)
         @ 13   java.lang.String::<init> (62 bytes)   inline (hot)
           @ 1   java.lang.Object::<init> (1 bytes)   inline (hot)
           @ 55   java.util.Arrays::copyOfRange (63 bytes)   inline (hot)
             @ 54   java.lang.Math::min (11 bytes)   (intrinsic)
             @ 57   java.lang.System::arraycopy (0 bytes)   (intrinsic)
    

    You might notice that 7u55 version is shallower, and it looks like nothing is called after StringBuilder methods -- this is a good indication the string optimizations are in effect. Indeed, if you run 7u55 with -XX:-OptimizeStringConcat, the subcalls will reappear, and performance drops to 8u5 levels.

  • OK, so we need to figure out why 8u5 does not do the same optimization. Grep http://hg.openjdk.java.net/jdk9/jdk9/hotspot for "StringBuilder" to figure out where VM handles the StringConcat optimization; this will get you into src/share/vm/opto/stringopts.cpp

  • hg log src/share/vm/opto/stringopts.cpp to figure out the latest changes there. One of the candidates would be:

    changeset:   5493:90abdd727e64
    user:        iveresov
    date:        Wed Oct 16 11:13:15 2013 -0700
    summary:     8009303: Tiered: incorrect results in VM tests stringconcat...
    
  • Look for the review threads on OpenJDK mailing lists (easy enough to google for changeset summary): http://mail.openjdk.java.net/pipermail/hotspot-compiler-dev/2013-October/012084.html

  • Spot "String concat optimization optimization collapses the pattern [...] into a single allocation of a string and forming the result directly. All possible deopts that may happen in the optimized code restart this pattern from the beginning (starting from the StringBuffer allocation). That means that the whole pattern must me side-effect free." Eureka?

  • Write out the contrasting benchmark:

    @Fork(5)
    @Warmup(iterations = 5)
    @Measurement(iterations = 5)
    @BenchmarkMode(Mode.AverageTime)
    @OutputTimeUnit(TimeUnit.NANOSECONDS)
    @State(Scope.Benchmark)
    public class IntStr {
        private int counter;
    
        @GenerateMicroBenchmark
        public String inlineSideEffect() {
            return new StringBuilder().append(counter++).toString();
        }
    
        @GenerateMicroBenchmark
        public String spliceSideEffect() {
            int cnt = counter++;
            return new StringBuilder().append(cnt).toString();
        }
    }
    
  • Measure it on JDK 7u55, seeing the same performance for inlined/spliced side effects:

    Benchmark                       Mode   Samples         Mean   Mean error    Units
    o.s.IntStr.inlineSideEffect     avgt        25       65.460        1.747    ns/op
    o.s.IntStr.spliceSideEffect     avgt        25       64.414        1.323    ns/op
    
  • Measure it on JDK 8u5, seeing the performance degradation with the inlined effect:

    Benchmark                       Mode   Samples         Mean   Mean error    Units
    o.s.IntStr.inlineSideEffect     avgt        25       84.953        2.274    ns/op
    o.s.IntStr.spliceSideEffect     avgt        25       65.386        1.194    ns/op
    
  • Submit the bug report (https://bugs.openjdk.java.net/browse/JDK-8043677) to discuss this behavior with VM guys. The rationale for original fix is rock solid, it is interesting however if we can/should get back this optimization in some trivial cases like these.

  • ???

  • PROFIT.

And yeah, I should post the results for the benchmark which moves the increment from the StringBuilder chain, doing it before the entire chain. Also, switched to average time, and ns/op. This is JDK 7u55:

Benchmark                      Mode   Samples         Mean   Mean error    Units
o.s.IntStr.integerToString     avgt        25      153.805        1.093    ns/op
o.s.IntStr.stringBuilder0      avgt        25      128.284        6.797    ns/op
o.s.IntStr.stringBuilder1      avgt        25      131.524        3.116    ns/op
o.s.IntStr.stringBuilder2      avgt        25      254.384        9.204    ns/op
o.s.IntStr.stringFormat        avgt        25     2302.501      103.032    ns/op

And this is 8u5:

Benchmark                      Mode   Samples         Mean   Mean error    Units
o.s.IntStr.integerToString     avgt        25      153.032        3.295    ns/op
o.s.IntStr.stringBuilder0      avgt        25      127.796        1.158    ns/op
o.s.IntStr.stringBuilder1      avgt        25      131.585        1.137    ns/op
o.s.IntStr.stringBuilder2      avgt        25      250.980        2.773    ns/op
o.s.IntStr.stringFormat        avgt        25     2123.706       25.105    ns/op

stringFormat is actually a bit faster in 8u5, and all other tests are the same. This solidifies the hypothesis the side-effect breakage in SB chains in the major culprit in the original question.


I think this has to do with the CompileThreshold flag which controls when the byte code is compiled into machine code by JIT.

The Oracle JDK has a default count of 10,000 as document at http://www.oracle.com/technetwork/java/javase/tech/vmoptions-jsp-140102.html.

Where OpenJDK I couldn't find a latest document on this flag; but some mail threads suggest a much lower threshold: http://mail.openjdk.java.net/pipermail/hotspot-compiler-dev/2010-November/004239.html

Also, try turn on / off the Oracle JDK flags like -XX:+UseCompressedStrings and -XX:+OptimizeStringConcat. I am not sure if those flags are turned on by default on OpenJDK though. Could someone please suggest.

One experiement you can do, is to firstly run the program by a lot of times, say, 30,000 loops, do a System.gc() and then try to look at the performance. I believe they would yield the same.

And I assume your GC setting is the same too. Otherwise you are allocating a lot of objects and the GC might well be the major part of your run time.

참고URL : https://stackoverflow.com/questions/23756966/why-is-stringbuilderappendint-faster-in-java-7-than-in-java-8

반응형