글 작성자: 개발섭

검증 안된 것에 대해서 당당하게 말하기! ㅋㅋ;

스터디 도중에 페이스북에서 본 기억이 나서 공유했었던 의제이다. +가 StringBuilder보다 빠릅니다! 라고 당당하게 언급했었는데, 막상 스터디 팀원들의 질문 공세에 어버버하며 재대로 대답을 못해드리게 되었다. 그 덕분에 자세히 찾아보게 되었는데, 원글을 읽고 글의 정확성을 한번 확인해보는 과정이 필요했었으며 심지어 더 자세히 아래쪽을 보면 더 정확하게 설명이 있음에도 불구하고 +가 StringBuilder보다 빠릅니다!  더 빠르다는 것에 심취해 있어 잘못된 정보를 전달할뻔 했었다.  

그래서 그런 정보를 바로 잡기도하며, 지식 정리도 해보고 싶어서 작성하게 되었다. 

+ 연산이 진짜 빠른가?

String 연산은 코딩테스트와 여러 상황에서 + 보다 StringBuilder가 사용되는 경우는 많았습니다. 특히, 백준과 같은 코딩테스트 사이트에서 일정시간 1초로 지정되어있고 정말 많은 수의 문자열 연산을 해야하는 경우 +를 하게된다면 시간 초과가 나오는 경우가 많았는데요.

예전에 여럼풋이 기억난 것중  디 컴파일링시 + 연산은 대부분 StringBuilder를 통해 append되는 모습을 볼 수 있다. 자바 내부적으로는 실제로 + 연산을 모두 .append화 시키는데 "+" 연산자체가 빠른가에 대해서 논의 하는거 자체가 의마가 없지 않나라는 생각이 많이 들었다. 

이렇게 혼자서 상상한것은 의미가 없어보이고 직접 해보는게 더 좋아보였고, 확인해보았다.

그래서 직접 해보았다

10~100만이상 for문을 돌리는 방식으로 실험을 해보았다. 

public static void main(String[] args) {
    long start = System.currentTimeMillis();
    String test = "aa"; //StringBuilder test = new StringBuilder("aa");
    for (int i = 0; i <100000; i++) {
        test= test+"aa"; // test.append("aa");
    }
    long end = System.currentTimeMillis()-start;
    System.out.println(end);
}

3797 vs 15 정도로 +StringBuilder 차이가 많이 났다. 200배가량 차이가 남..

 

내가 생각했었던 문자열을 컴파일시 StringBuilder로 묶어주는게 아닌가 했었는데, 막상 Compile시에서는 합쳐지는게 보여지진 않았다. 그럼 내가 생각했던것은 뭐지?

오히려 신기한점은 문자열을 아예 하나의 문자로써 완전히 붙혀버렸다. 

String a = "a";
String b = "1234555";
System.out.println(a+b+"123455"+1);
System.out.println("안녕, "+"안녕, "+"안녕, "+"안녕, "+"안녕, "+"안녕, "+"안녕, "+"안녕, "+"안녕, "+"안녕, "+"안녕, "+"안녕, "+"안녕, "+"끝!");

//컴파일시 이렇게 된다. 1이나 문자열이 그냥 아예 한 문자열로 붙혀져서 처리된다.

System.out.println(a + b + "1234551");
System.out.println("안녕, 안녕, 안녕, 안녕, 안녕, 안녕, 안녕, 안녕, 안녕, 안녕, 안녕, 안녕, 안녕, 끝!"); 

 

근데 바이트 코드를 까보니까  버전별로 다른 방식으로 처리되는 점을 확인해봤는데, 그 점은 다음과 같다.

 

Java 11버전

#0:makeConcatWithConstants:(Ljava/lang/String;)Ljava/lang/String;

이렇게 처리한다면...

 

8버전은 이렇게 StringBuilder로 붙임

19: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
22: ldc           #7                  // String 1234
24: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;

내가 생각했던것이 바이트 코드에서 실현이 되었던것.. 그래서 Java 8과 StringBuilder과 +를 비교해보면 더 명확하지 않을까 싶어서 또 시도해보았다.

덧붙이기 Java 8에서 +가 느린이유 SB에 비해서 훨씬 훨씬 느린이유

+로 붙이면 바이트 코드는 다음과 같이 나오고

 public static void main(java.lang.String[]);
    Code:
       0: ldc           #2                  // String !23
       2: astore_1
       3: iconst_0
       4: istore_2
       5: iload_2
       6: ldc           #3                  // int 100000
       8: if_icmpge     37
      11: new           #4                  // class java/lang/StringBuilder
      14: dup
      15: invokespecial #5                  // Method java/lang/StringBuilder."<init>":()V
      18: aload_1
      19: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      22: ldc           #7                  // String 1234
      24: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      27: invokevirtual #8                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      30: astore_1
      31: iinc          2, 1
      34: goto          5
      37: getstatic     #9                  // Field java/lang/System.out:Ljava/io/PrintStream;
      40: aload_1
      41: invokevirtual #10                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      44: return

StringBuilder로 하면 아래처럼 나오는데

Code:
       0: new           #2                  // class java/lang/StringBuilder
       3: dup
       4: ldc           #3                  // String !23
       6: invokespecial #4                  // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
       9: astore_1
      10: iconst_0
      11: istore_2
      12: iload_2
      13: ldc           #5                  // int 100000
      15: if_icmpge     31
      18: aload_1
      19: ldc           #6                  // String 1234
      21: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      24: pop
      25: iinc          2, 1
      28: goto          12
      31: getstatic     #8                  // Field java/lang/System.out:Ljava/io/PrintStream;
      34: aload_1
      35: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
      38: return

요약하자면 다음과 같다. 

+ 한경우 다음 로직을 타는것 처럼 보인다.

 

StringBuilder를 생성 초기화 적제 -> Append후 붙혀주기 -> toString으로.. 바꾼다음에 적재 -> 그리고 다시 초기화 적제-> Append를하니까

 

느릴 수 밖에 없는 구조인 것이다. 물론 바이트코드를 명확하게 해석 하지 않아서 조금의 차이는 있을것 같지만 속도를 잡아먹는 부분이 분명히 존재했었다. 

 

Java 11버전 (9버전부터는 makeConcatWithConstants을 통해서 획기적인 개선 그래서 둘 다 똑같음.) +를 쓰면 이렇게 나와버림.

 Code:
       0: ldc           #2                  // String !23
       2: astore_1
       3: iconst_0
       4: istore_2
       5: iload_2
       6: ldc           #3                  // int 100000
       8: if_icmpge     24
      11: aload_1
      12: invokedynamic #4,  0              // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;)Ljava/lang/String;

StringBuilder를 쓰는 경우는 똑같음 근데 속도는 StringBuilder가 좀더 빠름. 대신 9버전의 String을 합치는 방식이 8버전보다는 10배이상은 빨라보였다. 

//이런 코드를 작성하면...
Long start = System.currentTimeMillis();
String a = "1234";
for (int i = 0; i <100000; i++) {
    a+="92292929";
}
System.out.println(System.currentTimeMillis()-start);

Java 11: 4318 Java 8: 31110 이었는데,  진짜 10배 가량 차이가 난다.

Compile방식의 차이가 속도의 차이가 발생한다.

 

이런 결과를 만들어낸 makeConcatWithConstants 로 개선된 이유에 대해서 찾아봤는데, 이 로직을 찾는건 좀 어려웠고, 자바 9부터 Indify String Concatenation 를 통해서, 개선 사항에 포함되어있었는데...

 

원문: http://openjdk.java.net/jeps/280

 

JEP 280: Indify String Concatenation

JEP 280: Indify String Concatenation OwnerAleksey ShipilevTypeFeatureScopeSEStatusClosed / DeliveredRelease9Componenttools / javacDiscussioncore dash libs dash dev at openjdk dot java dot net, compiler dash dev at openjdk dot java dot net, hotspot

openjdk.java.net

 

오역이 있을가능성이 있지만, 맥락적으로 파악해 보았다.

 

결국 8버전 바이트코드와 같이 StringBuilder를 잘못 쓰는 케이스가 있기 때문에, 이러한 String을 붙이는 것에 대해서 개선을 하였고, 그것이 makeConcatWithConstants로 조금 더 빠른 속도로 String +를 개선하는 방향 버전 업그레이드 방향을 잡은듯했고, 실제로 반영이 되어있다.

 

makeConcatWithConstants의 작동 로직역시 궁금하였으나, 부족한 검색 실력으로 찾지는 못했고... 그리고 찾더라도 너무너무 딥다이브하게 들어가는 것 같아서 일단 개선 되었다는 점 그리고 개선의 이유까지만 찾아서 마무리했다.

결론

'+' 는 실제로 StringBuilder보다는 느린것은 사실... 하지만 속도 자체는 8에 비해서는 그 이후 버전이 말 그대로 10배나 차이날정도로 빨라졌음. 매우 많은 수(최소 10만 이상)를 붙이는 것이 아닌 이상.... +를 통해서 얻는 손해는 커지 않아 보인다.

적은 갯수의 String을 합치는 것에 대해서는 오로지 속도적인 측면에서는 그렇게 느리지 않다고 봐도 좋을 것 같다.

 

출처:

http://wonwoo.ml/index.php/post/1039

 

java bytecode - 머루의개발블로그

오늘은 java 바이트코드에 대해서 잠깐 살펴보자. 우리는 거의 바이트코드를 볼일이 없다. 바이트코드 레벨까지 내려갈 필요가 없기에 딱히 볼일이 없다. 바이트코드를 상세하게 볼건 없고 그냥

wonwoo.ml

 

https://stackoverflow.com/questions/46512888/how-is-string-concatenation-implemented-in-java-9

 

How is String concatenation implemented in Java 9?

As written in JEP 280: Indify String Concatenation: Change the static String-concatenation bytecode sequence generated by javac to use invokedynamic calls to JDK library functions. This will ena...

stackoverflow.com