for 循环中的字符串串联。Java 9

2022-09-04 08:43:50

如果我错了,请纠正我。在Java 8中,出于性能原因,当通过“+”运算符连接多个字符串时,调用了StringBuffer。创建一堆中间字符串对象并污染字符串池的问题得到了“解决”。

那么Java 9呢?有一个新功能作为 Invokedynamic 添加。还有一个能更好地解决这个问题的新类,StringConcatFactory。

String result = "";
List<String> list = Arrays.asList("a", "b", "c");
for (String n : list) {
 result+=n;
}

我的问题是:在这个循环中创建了多少个对象?是否有任何中间人对象?我该如何验证这一点?


答案 1

为了记录,这是一个测试...JMH

@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@Warmup(iterations = 5, time = 5, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 5, timeUnit = TimeUnit.SECONDS)
@State(Scope.Thread)
public class LoopTest {

    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder().include(LoopTest.class.getSimpleName())
                .jvmArgs("-ea", "-Xms10000m", "-Xmx10000m")
                .shouldFailOnError(true)
                .build();
        new Runner(opt).run();
    }

    @Param(value = {"1000", "10000", "100000"})
    int howmany;

    @Fork(1)
    @Benchmark
    public String concatBuilder(){
        StringBuilder sb = new StringBuilder();
        for(int i=0;i<howmany;++i){
            sb.append(i);
        }
        return sb.toString();
    }

    @Fork(1)
    @Benchmark
    public String concatPlain(){
        String result = "";
        for(int i=0;i<howmany;++i){
            result +=i;
        }
        return result;
    }
}

产生我并不真正期望的结果(仅用于此处所示):100000

LoopTest.concatPlain       100000  avgt    5  3902.711 ± 67.215  ms/op
LoopTest.concatBuilder     100000  avgt    5     1.850 ±  0.574  ms/op

答案 2

我的问题是:在这个循环中创建了多少个对象?是否有任何中间对象?我该如何验证?

剧情透露:

JVM 不会尝试省略循环中的中间对象 - 因此在使用普通串联时将创建它们。

让我们先看一下字节码。我使用了@Eugene提供的性能测试,为java8编译它们,然后为java9编译它们。以下是我们要比较的2种方法:

public String concatBuilder() {
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < howmany; ++i) {
        sb.append(i);
    }
    return sb.toString();
}

public String concatPlain() {
    String result = "";
    for (int i = 0; i < howmany; ++i) {
        result = result + i;
    }
    return result;
}

我的Java版本如下:

java version "1.8.0_131"
Java(TM) SE Runtime Environment (build 1.8.0_131-b11)
Java HotSpot(TM) 64-Bit Server VM (build 25.131-b11, mixed mode)

java version "9.0.4"
Java(TM) SE Runtime Environment (build 9.0.4+11)
Java HotSpot(TM) 64-Bit Server VM (build 9.0.4+11, mixed mode)

JMH版本是1.20

这是我从中获得的输出:javap -c LoopTest.class

显式使用的方法对于 java8 和 java9 看起来完全相同:concatBuilder()StringBuilder

public java.lang.String concatBuilder();
Code:
   0: new           #17                 // class java/lang/StringBuilder
   3: dup
   4: invokespecial #18                 // Method java/lang/StringBuilder."<init>":()V
   7: astore_1
   8: iconst_0
   9: istore_2
  10: iload_2
  11: aload_0
  12: getfield      #19                 // Field howmany:I
  15: if_icmpge     30
  18: aload_1
  19: iload_2
  20: invokevirtual #20                 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
  23: pop
  24: iinc          2, 1
  27: goto          10
  30: aload_1
  31: invokevirtual #21                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
  34: areturn

请注意,调用 发生在循环内部,而 调用在循环外部。这很重要 - 这意味着不会创建中间对象。在java8字节码中,它有点不同:StringBuilder.appendStringBuilder.toString

Java8 中的方法:concatPlain()

public java.lang.String concatPlain();
Code:
   0: ldc           #22                 // String
   2: astore_1
   3: iconst_0
   4: istore_2
   5: iload_2
   6: aload_0
   7: getfield      #19                 // Field howmany:I
  10: if_icmpge     38
  13: new           #17                 // class java/lang/StringBuilder
  16: dup
  17: invokespecial #18                 // Method java/lang/StringBuilder."<init>":()V
  20: aload_1
  21: invokevirtual #23                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  24: iload_2
  25: invokevirtual #20                 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
  28: invokevirtual #21                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
  31: astore_1
  32: iinc          2, 1
  35: goto          5
  38: aload_1
  39: areturn

你可以看到,在java8中,两者都在循环语句中被调用,这意味着它甚至不会尝试省略中间对象的创建!可以在下面的代码中描述它:StringBuilder.appendStringBuilder.toString

public String concatPlain() {
    String result = "";
    for (int i = 0; i < howmany; ++i) {
        result = result + i;
        result = new StringBuilder().append(result).append(i).toString();
    }
    return result;
}

这解释了 和 之间的性能差异(这是几千倍(!))。java9也出现了同样的问题 - 它不会试图避免循环中的中间对象,但它在循环中的表现比java8稍微好一些(添加了性能结果):concatPlain()concatBuilder()

方法 Java9:concatPlain()

public java.lang.String concatPlain();
Code:
   0: ldc           #22                 // String
   2: astore_1
   3: iconst_0
   4: istore_2
   5: iload_2
   6: aload_0
   7: getfield      #19                 // Field howmany:I
  10: if_icmpge     27
  13: aload_1
  14: iload_2
  15: invokedynamic #23,  0             // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;I)Ljava/lang/String;
  20: astore_1
  21: iinc          2, 1
  24: goto          5
  27: aload_1
  28: areturn

以下是性能结果:

JAVA 8:

# Run complete. Total time: 00:02:18

Benchmark               (howmany)  Mode  Cnt     Score      Error  Units
LoopTest.concatBuilder     100000  avgt    5     2.098 ±    0.027  ms/op
LoopTest.concatPlain       100000  avgt    5  6908.737 ± 1227.681  ms/op

JAVA 9:

对于 java 9,使用 定义了不同的策略。我尝试了所有方法:-Djava.lang.invoke.stringConcat

默认值 (MH_INLINE_SIZED_EXACT):

# Run complete. Total time: 00:02:30
Benchmark               (howmany)  Mode  Cnt     Score    Error  Units
LoopTest.concatBuilder     100000  avgt    5     1.625 ±  0.015  ms/op
LoopTest.concatPlain       100000  avgt    5  4812.022 ± 73.453  ms/op

-Djava.lang.invoke.stringConcat=BC_SB

# Run complete. Total time: 00:02:28
Benchmark               (howmany)  Mode  Cnt     Score    Error  Units
LoopTest.concatBuilder     100000  avgt    5     1.501 ±  0.024  ms/op
LoopTest.concatPlain       100000  avgt    5  4803.543 ± 53.825  ms/op

-Djava.lang.invoke.stringConcat=BC_SB_SIZED

# Run complete. Total time: 00:02:17
Benchmark               (howmany)  Mode  Cnt     Score     Error  Units
LoopTest.concatBuilder     100000  avgt    5     1.546 ±   0.027  ms/op
LoopTest.concatPlain       100000  avgt    5  4941.226 ± 422.704  ms/op

-Djava.lang.invoke.stringConcat=BC_SB_SIZED_EXACT

# Run complete. Total time: 00:02:45
Benchmark               (howmany)  Mode  Cnt      Score     Error  Units
LoopTest.concatBuilder     100000  avgt    5      1.560 ±   0.073  ms/op
LoopTest.concatPlain       100000  avgt    5  11390.665 ± 232.269  ms/op

-Djava.lang.invoke.stringConcat=BC_SB_SIZED_EXACT

# Run complete. Total time: 00:02:16
Benchmark               (howmany)  Mode  Cnt     Score     Error  Units
LoopTest.concatBuilder     100000  avgt    5     1.616 ±   0.030  ms/op
LoopTest.concatPlain       100000  avgt    5  8524.200 ± 219.499  ms/op

-Djava.lang.invoke.stringConcat=MH_SB_SIZED_EXACT

# Run complete. Total time: 00:02:17
Benchmark               (howmany)  Mode  Cnt     Score     Error  Units
LoopTest.concatBuilder     100000  avgt    5     1.633 ±   0.058  ms/op
LoopTest.concatPlain       100000  avgt    5  8499.228 ± 972.832  ms/op

-Djava.lang.invoke.stringConcat=MH_INLINE_SIZED_EXACT(是的,这是默认的,但我决定为了实验的清晰而明确设置它)

# Run complete. Total time: 00:02:23
Benchmark               (howmany)  Mode  Cnt     Score    Error  Units
LoopTest.concatBuilder     100000  avgt    5     1.654 ±  0.015  ms/op
LoopTest.concatPlain       100000  avgt    5  4812.231 ± 54.061  ms/op

我决定调查内存使用情况,但除了java9消耗更多内存之外,没有发现任何有趣的东西。附上截图,以防有人感兴趣。当然,它们是在实际性能测量之后制作的,但不是在实际性能测量期间进行的。

Java8 concatBuilder(): Java8 concatBuilder() Java8 concatPlain(): enter image description here Java9 concatBuilder(): enter image description here Java9 concatPlain(): enter image description here

所以,是的,回答你的问题,我可以说java8和java9都不能避免在循环中创建中间对象。

更新:

正如@Eugene裸字节码migt毫无意义,因为JIT在运行时做了很多优化,这在我看来是合乎逻辑的,所以我决定添加由JIT代码优化的输出(由捕获)。-XX:CompileCommand=print,*LoopTest.concatPlain

JAVA 8:

0x00007f8c2d216d29: callq   0x7f8c2d0fdea0    ; OopMap{rsi=Oop [96]=Oop off=1550}
                                            ;*synchronization entry
                                            ; - org.sample.LoopTest::concatPlain@-1 (line 73)
                                            ;   {runtime_call}
0x00007f8c2d216d2e: jmpq    0x7f8c2d216786
0x00007f8c2d216d33: mov     %rdx,%rdx
0x00007f8c2d216d36: callq   0x7f8c2d0fa1a0    ; OopMap{r9=Oop [96]=Oop off=1563}
                                            ;*new  ; - org.sample.LoopTest::concatPlain@13 (line 75)
                                            ;   {runtime_call}
0x00007f8c2d216d3b: jmpq    0x7f8c2d2167e6
0x00007f8c2d216d40: mov     %rbx,0x8(%rsp)
0x00007f8c2d216d45: movq    $0xffffffffffffffff,(%rsp)
0x00007f8c2d216d4d: callq   0x7f8c2d0fdea0    ; OopMap{r9=Oop [96]=Oop rax=Oop off=1586}
                                            ;*synchronization entry
                                            ; - java.lang.StringBuilder::<init>@-1 (line 89)
                                            ; - org.sample.LoopTest::concatPlain@17 (line 75)
                                            ;   {runtime_call}
0x00007f8c2d216d52: jmpq    0x7f8c2d21682d
0x00007f8c2d216d57: mov     %rbx,0x8(%rsp)
0x00007f8c2d216d5c: movq    $0xffffffffffffffff,(%rsp)
0x00007f8c2d216d64: callq   0x7f8c2d0fdea0    ; OopMap{r9=Oop [96]=Oop rax=Oop off=1609}
                                            ;*synchronization entry
                                            ; - java.lang.AbstractStringBuilder::<init>@-1 (line 67)
                                            ; - java.lang.StringBuilder::<init>@3 (line 89)
                                            ; - org.sample.LoopTest::concatPlain@17 (line 75)
                                            ;   {runtime_call}
0x00007f8c2d216d69: jmpq    0x7f8c2d216874
0x00007f8c2d216d6e: mov     %rbx,0x8(%rsp)
0x00007f8c2d216d73: movq    $0xffffffffffffffff,(%rsp)
0x00007f8c2d216d7b: callq   0x7f8c2d0fdea0    ; OopMap{r9=Oop [96]=Oop rax=Oop off=1632}
                                            ;*synchronization entry
                                            ; - java.lang.Object::<init>@-1 (line 37)
                                            ; - java.lang.AbstractStringBuilder::<init>@1 (line 67)
                                            ; - java.lang.StringBuilder::<init>@3 (line 89)
                                            ; - org.sample.LoopTest::concatPlain@17 (line 75)
                                            ;   {runtime_call}
0x00007f8c2d216d80: jmpq    0x7f8c2d2168bb
0x00007f8c2d216d85: callq   0x7f8c2d0faa60    ; OopMap{r9=Oop [96]=Oop r13=Oop off=1642}
                                            ;*newarray
                                            ; - java.lang.AbstractStringBuilder::<init>@6 (line 68)
                                            ; - java.lang.StringBuilder::<init>@3 (line 89)
                                            ; - org.sample.LoopTest::concatPlain@17 (line 75)
                                            ;   {runtime_call}
0x00007f8c2d216d8a: jmpq    0x7f8c2d21693a
0x00007f8c2d216d8f: mov     %rdx,0x8(%rsp)
0x00007f8c2d216d94: movq    $0xffffffffffffffff,(%rsp)
0x00007f8c2d216d9c: callq   0x7f8c2d0fdea0    ; OopMap{r9=Oop [96]=Oop r13=Oop off=1665}
                                            ;*synchronization entry
                                            ; - java.lang.StringBuilder::append@-1 (line 136)
                                            ; - org.sample.LoopTest::concatPlain@21 (line 75)
                                            ;   {runtime_call}
0x00007f8c2d216da1: jmpq    0x7f8c2d216a1c
0x00007f8c2d216da6: mov     %rdx,0x8(%rsp)
0x00007f8c2d216dab: movq    $0xffffffffffffffff,(%rsp)
0x00007f8c2d216db3: callq   0x7f8c2d0fdea0    ; OopMap{[80]=Oop [96]=Oop off=1688}
                                            ;*synchronization entry
                                            ; - java.lang.StringBuilder::append@-1 (line 208)
                                            ; - org.sample.LoopTest::concatPlain@25 (line 75)
                                            ;   {runtime_call}
0x00007f8c2d216db8: jmpq    0x7f8c2d216b08
0x00007f8c2d216dbd: mov     %rdx,0x8(%rsp)
0x00007f8c2d216dc2: movq    $0xffffffffffffffff,(%rsp)
0x00007f8c2d216dca: callq   0x7f8c2d0fdea0    ; OopMap{[80]=Oop [96]=Oop off=1711}
                                            ;*synchronization entry
                                            ; - java.lang.StringBuilder::toString@-1 (line 407)
                                            ; - org.sample.LoopTest::concatPlain@28 (line 75)
                                            ;   {runtime_call}
0x00007f8c2d216dcf: jmpq    0x7f8c2d216bf8
0x00007f8c2d216dd4: mov     %rdx,%rdx
0x00007f8c2d216dd7: callq   0x7f8c2d0fa1a0    ; OopMap{[80]=Oop [96]=Oop off=1724}
                                            ;*new  ; - java.lang.StringBuilder::toString@0 (line 407)
                                            ; - org.sample.LoopTest::concatPlain@28 (line 75)
                                            ;   {runtime_call}
0x00007f8c2d216ddc: jmpq    0x7f8c2d216c39
0x00007f8c2d216de1: mov     %rax,0x8(%rsp)
0x00007f8c2d216de6: movq    $0x23,(%rsp)
0x00007f8c2d216dee: callq   0x7f8c2d0fdea0    ; OopMap{[96]=Oop [104]=Oop off=1747}
                                            ;*goto
                                            ; - org.sample.LoopTest::concatPlain@35 (line 74)
                                            ;   {runtime_call}
0x00007f8c2d216df3: jmpq    0x7f8c2d216cae

如您所见,在 goto 之前被调用,这意味着一切都发生在循环中。java9 的情况类似 - 在 goto 命令之前调用。StringBuilder::toStringStringConcatHelper::newString

JAVA 9:

0x00007fa1256548a4: mov     %ebx,%r13d
0x00007fa1256548a7: sub     0xc(%rsp),%r13d   ;*isub {reexecute=0 rethrow=0 return_oop=0}
                                            ; - java.lang.StringConcatHelper::prepend@5 (line 329)
                                            ; - java.lang.invoke.DirectMethodHandle$Holder::invokeStatic@16
                                            ; - java.lang.invoke.LambdaForm$BMH/127835623::reinvoke@172
                                            ; - java.lang.invoke.LambdaForm$MH/1587176117::linkToTargetMethod@6
                                            ; - org.sample.LoopTest::concatPlain@15 (line 75)

0x00007fa1256548ac: test    %r13d,%r13d
0x00007fa1256548af: jl      0x7fa125654b11
0x00007fa1256548b5: mov     %r13d,%r10d
0x00007fa1256548b8: add     %r9d,%r10d
0x00007fa1256548bb: mov     0x20(%rsp),%r11d
0x00007fa1256548c0: cmp     %r10d,%r11d
0x00007fa1256548c3: jb      0x7fa125654b11    ;*invokestatic arraycopy {reexecute=0 rethrow=0 return_oop=0}
                                            ; - java.lang.String::getBytes@22 (line 2993)
                                            ; - java.lang.StringConcatHelper::prepend@11 (line 330)
                                            ; - java.lang.invoke.DirectMethodHandle$Holder::invokeStatic@16
                                            ; - java.lang.invoke.LambdaForm$BMH/127835623::reinvoke@172
                                            ; - java.lang.invoke.LambdaForm$MH/1587176117::linkToTargetMethod@6
                                            ; - org.sample.LoopTest::concatPlain@15 (line 75)

0x00007fa1256548c9: test    %r9d,%r9d
0x00007fa1256548cc: jbe     0x7fa1256548ef
0x00007fa1256548ce: movsxd  %r9d,%rdx
0x00007fa1256548d1: lea     (%r12,%r8,8),%r10  ;*getfield value {reexecute=0 rethrow=0 return_oop=0}
                                            ; - java.lang.String::length@1 (line 669)
                                            ; - java.lang.StringConcatHelper::mixLen@2 (line 116)
                                            ; - java.lang.invoke.DirectMethodHandle$Holder::invokeStatic@11
                                            ; - java.lang.invoke.LambdaForm$BMH/127835623::reinvoke@105
                                            ; - java.lang.invoke.LambdaForm$MH/1587176117::linkToTargetMethod@6
                                            ; - org.sample.LoopTest::concatPlain@15 (line 75)

0x00007fa1256548d5: lea     0x10(%r12,%r8,8),%rdi
0x00007fa1256548da: mov     %rcx,%r10
0x00007fa1256548dd: lea     0x10(%rcx,%r13),%rsi
0x00007fa1256548e2: movabs  $0x7fa11db9d640,%r10
0x00007fa1256548ec: callq   %r10              ;*invokestatic arraycopy {reexecute=0 rethrow=0 return_oop=0}
                                            ; - java.lang.String::getBytes@22 (line 2993)
                                            ; - java.lang.StringConcatHelper::prepend@11 (line 330)
                                            ; - java.lang.invoke.DirectMethodHandle$Holder::invokeStatic@16
                                            ; - java.lang.invoke.LambdaForm$BMH/127835623::reinvoke@172
                                            ; - java.lang.invoke.LambdaForm$MH/1587176117::linkToTargetMethod@6
                                            ; - org.sample.LoopTest::concatPlain@15 (line 75)

0x00007fa1256548ef: cmp     0xc(%rsp),%ebx
0x00007fa1256548f3: jne     0x7fa125654cb9    ;*ifeq {reexecute=0 rethrow=0 return_oop=0}
                                            ; - java.lang.StringConcatHelper::newString@1 (line 343)
                                            ; - java.lang.invoke.DirectMethodHandle$Holder::invokeStatic@14
                                            ; - java.lang.invoke.LambdaForm$BMH/127835623::reinvoke@194
                                            ; - java.lang.invoke.LambdaForm$MH/1587176117::linkToTargetMethod@6
                                            ; - org.sample.LoopTest::concatPlain@15 (line 75)

0x00007fa1256548f9: mov     0x60(%r15),%rax
0x00007fa1256548fd: mov     %rax,%r10
0x00007fa125654900: add     $0x18,%r10
0x00007fa125654904: cmp     0x70(%r15),%r10
0x00007fa125654908: jnb     0x7fa125654aa5
0x00007fa12565490e: mov     %r10,0x60(%r15)
0x00007fa125654912: prefetchnta 0x100(%r10)
0x00007fa12565491a: mov     0x18(%rsp),%rsi
0x00007fa12565491f: mov     0xb0(%rsi),%r10
0x00007fa125654926: mov     %r10,(%rax)
0x00007fa125654929: movl    $0xf80002da,0x8(%rax)  ;   {metadata('java/lang/String')}
0x00007fa125654930: mov     %r12d,0xc(%rax)
0x00007fa125654934: mov     %r12,0x10(%rax)   ;*new {reexecute=0 rethrow=0 return_oop=0}
                                            ; - java.lang.StringConcatHelper::newString@36 (line 346)
                                            ; - java.lang.invoke.DirectMethodHandle$Holder::invokeStatic@14
                                            ; - java.lang.invoke.LambdaForm$BMH/127835623::reinvoke@194
                                            ; - java.lang.invoke.LambdaForm$MH/1587176117::linkToTargetMethod@6
                                            ; - org.sample.LoopTest::concatPlain@15 (line 75)

0x00007fa125654938: mov     0x30(%rsp),%r10
0x00007fa12565493d: shr     $0x3,%r10
0x00007fa125654941: mov     %r10d,0xc(%rax)   ;*synchronization entry
                                            ; - java.lang.StringConcatHelper::newString@-1 (line 343)
                                            ; - java.lang.invoke.DirectMethodHandle$Holder::invokeStatic@14
                                            ; - java.lang.invoke.LambdaForm$BMH/127835623::reinvoke@194
                                            ; - java.lang.invoke.LambdaForm$MH/1587176117::linkToTargetMethod@6
                                            ; - org.sample.LoopTest::concatPlain@15 (line 75)

0x00007fa125654945: mov     0x8(%rsp),%ebx
0x00007fa125654949: incl    %ebx              ; ImmutableOopMap{rax=Oop [0]=Oop }
                                            ;*goto {reexecute=1 rethrow=0 return_oop=0}
                                            ; - org.sample.LoopTest::concatPlain@24 (line 74)

0x00007fa12565494b: test    %eax,0x1a8996af(%rip)  ;*goto {reexecute=0 rethrow=0 return_oop=0}
                                            ; - org.sample.LoopTest::concatPlain@24 (line 74)
                                            ;   {poll}