Skip to content

Instantly share code, notes, and snippets.

@benelog
Last active April 14, 2023 13:42
Show Gist options
  • Save benelog/b81b4434fb8f2220cd0e900be1634753 to your computer and use it in GitHub Desktop.
Save benelog/b81b4434fb8f2220cd0e900be1634753 to your computer and use it in GitHub Desktop.
String 최적화 JDK 1.5

jdk1.5에서 String 더하기의 컴파일시의 최적화]

String 클래스를 "+"로 반복해서 더하는 연산을 어떤 경우에 컴파일러에서 자동으로 최적화해주는지 알고 있다면 보다 융퉁성 있게 쓸 수가 있습니다.

몇년전에 javaservice.net에서의 String vs StringBuffer에 대한 논의에서도 이 이야기가 오고 갔었고, 그를 통해 제가 알게 되었던 바는 다음과 같습니다.

  1. 한줄에서 상수 String끼리만 더하는 것은 모두 합쳐진 문자열로 바꿔준다. 즉 String a= "a" + "b" + "c"; 라고 쓰면 String ="abc"; 로 알아서 컴파일해준다는 거죠.
  2. 한줄에서 상수와 다른 String 클래스를 더하는 것은 StringBuffer의 append, toString 메서드를 쓰는 코드로 준다. jdk 1.4 javadoc 의 StringBuffer API설명 에 명시되어 있네요.

String buffers are used by the compiler to implement the binary string concatenation operator . For example, the code:
x = "a" + 4 + "c"
is compiled to the equivalent of:
x = new StringBuffer().append("a").append(4).append("c").toString()
which creates a new string buffer (initially empty), appends the string representation of each operand to the string buffer in turn, and then converts the contents of the string buffer to a string. Overall, this avoids creating many temporary strings.

Java 1.5 이상에서는 String더하기가 StringBuilder로 치환된다는 것을 듣고나서, 이것을 직접 테스트 해보았습니다. jdk1.5의 API문서를 보시면 아시겠지만 StringBuilder는 동기화되지 않았다는 것이 SringBuffer와 차이점입니다.

참고로 컴파일은 이클립스에서 한 후 jad로 다시 역컴파일한 결과입니다.

원래 소스

public class StringTest {  
    public static void main(String[] args) {  
        String str0 = "It's a string....";  
        String str1 = "It's" + " a string" + "....";  
        String str2 = "It's a string...." + str0 + "000";  
        str2 = str0 + str1 + "1111" ;        
        str2 = str2 + "1111";  
        str2 += "1111";        
        for (int i=0;i<10;i++){  
            str2 = str2 + "1111";  
            str2 += "1111";        
        }  
    }  
}

jdk 1.4로 compile

public class StringTest{
 
    public StringTest()    {  
    }
 
    public static void main(String args[])    {  
        String str0 = "It's a string....";  
        String str1 = "It's a string....";  
        String str2 = "It's a string...." + str0 + "000";  
        str2 = str0 + str1 + "1111";  
        str2 = str2 + "1111";  
        str2 = str2 + "1111";  
        for(int i = 0; i < 10; i++)        {  
            str2 = str2 + "1111";  
            str2 = str2 + "1111";  
        }

    }  
} 

JDK 1.5로 compile

public class StringTest{  
    public StringTest()    {  
    }

    public static void main(String args[])    {  
        String str0 = "It's a string....";  
        String str1 = "It's a string....";  
        String str2 = (new StringBuilder("It's a string....")).append(str0).append("000").toString();  
        str2 = (new StringBuilder(String.valueOf(str0))).append(str1).append("1111").toString();  
        str2 = (new StringBuilder(String.valueOf(str2))).append("1111").toString();  
        str2 = (new StringBuilder(String.valueOf(str2))).append("1111").toString();  
        for(int i = 0; i < 10; i++)        {  
            str2 = (new StringBuilder(String.valueOf(str2))).append("1111").toString();  
            str2 = (new StringBuilder(String.valueOf(str2))).append("1111").toString();  
        }  
    }  
}

상수 더하기는 역시 String str1 = "It's" + " a string" + "...."; -> String str1 = "It's a string...."; 으로 양 버전 모두에서 바뀝니다. 상수와 상수가 아닌 것을 섞어서 더했는 때는 jdk1.4로 이클립스에서 컴파일한 결과로는 StringBuffer가 나타나지는 않네요. 그리고 1.5에서는 예상대로 StringBuilder가 나타납니다. jdk1.4에서 StringBuffer로 자동치환이 안되어서 나오는 것은 좀 이상하기는 해도, 반복문이 아닌 곳에서 스트링 한두개를 더하는 정도라면 최적화해 주지 않아도 한 두개의 객체가 더 생기는 정도일 것이니까 큰 성능의 차이는 없을 것 같습니다.

1.5에서는 반복문 안에서의 더하기도 StringBuilder로 바꿔주기는 하지만 매루프마다 새로운 StringBuilder 클래스를 생성하는 것이므로 String과 마찬가지로 필요없는 임시객체를 계속 만들게 됩니다. 즉 어떤 경우라도 반복문안에서 String 더하기에 "+"를 쓰지는 말아야 겠죠.

@benelog
Copy link
Author

benelog commented Aug 4, 2021

@apollyon4
StringBuffer로 변환되던건 JDK 1.4시절의 JavaDoc였습니다.
위의 본문에도 'jdk 1.4 javadoc' 라고 적혀있긴합니다.
JDK 1.5-8까지는 StringBuilder로 바뀝니다.

그리고 Java9부터는 최적화 방식이 완전히 달라졌습니다. InvokeDynamic을 이용해서 이미 컴파일된 코드라도 JDK버전업에 따른 최적화 여지를 더 남겨두었습니다. 그에 대해서는 https://dzone.com/articles/jdk-9jep-280-string-concatenations-will-never-be-t 에 설명되어 있습니다

디컴파일러의 옵션에는 따라서 원래의 코드에 가까운 복원을 위해 StringBuilder로 최적화된 코드를 안 보여주는 경우도 있습니다. cfr 이라는 디컴파일러도 그런 경우입니다.

http://www.benf.org/other/cfr/stringbuilder-vs-concatenation.html

즉, JDK의 버전과 디컴파일러의 옵션에 따라서 역컴파일된 소스는 다르게 보일수 있습니다.
바이트코드 분석을 하는것이 더 정확하기는 합니다.

직접 테스트도 해봤었는데요, 간단하게 아래 코드를 만들고,

public class Test {

        public static void main(String[] args) {
                String mid = "456";
                String msg = "123" + mid + "789";
                System.out.println(msg);
        }
}

컴파일후 바이트 코드를 보았습니다.

javac Test.java
javap -v Test

JDK 1.8.0_212

final 키워드가 없어도 원래 소스에서는 없었던 StringBuilder가 생성되는것이 확인이 됩니다.

Classfile /home/benelog/test/Test.class
  Last modified Dec 10, 2019; size 603 bytes
  MD5 checksum 0c5baeb5885e85240017fe287c711b85
  Compiled from "Test.java"
public class Test
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #12.#21        // java/lang/Object."<init>":()V
   #2 = String             #22            // 456
   #3 = Class              #23            // java/lang/StringBuilder
   #4 = Methodref          #3.#21         // java/lang/StringBuilder."<init>":()V
   #5 = String             #24            // 123
   #6 = Methodref          #3.#25         // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   #7 = String             #26            // 789
   #8 = Methodref          #3.#27         // java/lang/StringBuilder.toString:()Ljava/lang/String;
   #9 = Fieldref           #28.#29        // java/lang/System.out:Ljava/io/PrintStream;
  #10 = Methodref          #30.#31        // java/io/PrintStream.println:(Ljava/lang/String;)V
  #11 = Class              #32            // Test
  #12 = Class              #33            // java/lang/Object
  #13 = Utf8               <init>
  #14 = Utf8               ()V
  #15 = Utf8               Code
  #16 = Utf8               LineNumberTable
  #17 = Utf8               main
  #18 = Utf8               ([Ljava/lang/String;)V
  #19 = Utf8               SourceFile
  #20 = Utf8               Test.java
  #21 = NameAndType        #13:#14        // "<init>":()V
  #22 = Utf8               456
  #23 = Utf8               java/lang/StringBuilder
  #24 = Utf8               123
  #25 = NameAndType        #34:#35        // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #26 = Utf8               789
  #27 = NameAndType        #36:#37        // toString:()Ljava/lang/String;
  #28 = Class              #38            // java/lang/System
  #29 = NameAndType        #39:#40        // out:Ljava/io/PrintStream;
  #30 = Class              #41            // java/io/PrintStream
  #31 = NameAndType        #42:#43        // println:(Ljava/lang/String;)V
  #32 = Utf8               Test
  #33 = Utf8               java/lang/Object
  #34 = Utf8               append
  #35 = Utf8               (Ljava/lang/String;)Ljava/lang/StringBuilder;
  #36 = Utf8               toString
  #37 = Utf8               ()Ljava/lang/String;
  #38 = Utf8               java/lang/System
  #39 = Utf8               out
  #40 = Utf8               Ljava/io/PrintStream;
  #41 = Utf8               java/io/PrintStream
  #42 = Utf8               println
  #43 = Utf8               (Ljava/lang/String;)V
{
  public Test();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: ldc           #2                  // String 456
         2: astore_1
         3: new           #3                  // class java/lang/StringBuilder
         6: dup
         7: invokespecial #4                  // Method java/lang/StringBuilder."<init>":()V
        10: ldc           #5                  // String 123
        12: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        15: aload_1
        16: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        19: ldc           #7                  // String 789
        21: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        24: invokevirtual #8                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        27: astore_2
        28: getstatic     #9                  // Field java/lang/System.out:Ljava/io/PrintStream;
        31: aload_2
        32: invokevirtual #10                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        35: return
      LineNumberTable:
        line 6: 0
        line 7: 3
        line 8: 28
        line 10: 35
}
SourceFile: "Test.java"

JDK 13.0.1

StringBuilder 대신 InvokeDynamic + StringConcatFactory가 보입니다.

Classfile /home/benelog/test/Test.class
  Last modified Dec 10, 2019; size 875 bytes
  SHA-256 checksum 65f3c11abbb42e636b47d95bbfc1c05898d0ce9fb74273cb92062b52097cd2a8
  Compiled from "Test.java"
public class Test
  minor version: 0
  major version: 57
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #25                         // Test
  super_class: #2                         // java/lang/Object
  interfaces: 0, fields: 0, methods: 2, attributes: 3
Constant pool:
   #1 = Methodref          #2.#3          // java/lang/Object."<init>":()V
   #2 = Class              #4             // java/lang/Object
   #3 = NameAndType        #5:#6          // "<init>":()V
   #4 = Utf8               java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = String             #8             // 456
   #8 = Utf8               456
   #9 = InvokeDynamic      #0:#10         // #0:makeConcatWithConstants:(Ljava/lang/String;)Ljava/lang/String;
  #10 = NameAndType        #11:#12        // makeConcatWithConstants:(Ljava/lang/String;)Ljava/lang/String;
  #11 = Utf8               makeConcatWithConstants
  #12 = Utf8               (Ljava/lang/String;)Ljava/lang/String;
  #13 = Fieldref           #14.#15        // java/lang/System.out:Ljava/io/PrintStream;
  #14 = Class              #16            // java/lang/System
  #15 = NameAndType        #17:#18        // out:Ljava/io/PrintStream;
  #16 = Utf8               java/lang/System
  #17 = Utf8               out
  #18 = Utf8               Ljava/io/PrintStream;
  #19 = Methodref          #20.#21        // java/io/PrintStream.println:(Ljava/lang/String;)V
  #20 = Class              #22            // java/io/PrintStream
  #21 = NameAndType        #23:#24        // println:(Ljava/lang/String;)V
  #22 = Utf8               java/io/PrintStream
  #23 = Utf8               println
  #24 = Utf8               (Ljava/lang/String;)V
  #25 = Class              #26            // Test
  #26 = Utf8               Test
  #27 = Utf8               Code
  #28 = Utf8               LineNumberTable
  #29 = Utf8               main
  #30 = Utf8               ([Ljava/lang/String;)V
  #31 = Utf8               SourceFile
  #32 = Utf8               Test.java
  #33 = Utf8               BootstrapMethods
  #34 = MethodHandle       6:#35          // REF_invokeStatic java/lang/invoke/StringConcatFactory.makeConcatWithConstants:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
  #35 = Methodref          #36.#37        // java/lang/invoke/StringConcatFactory.makeConcatWithConstants:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
  #36 = Class              #38            // java/lang/invoke/StringConcatFactory
  #37 = NameAndType        #11:#39        // makeConcatWithConstants:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
  #38 = Utf8               java/lang/invoke/StringConcatFactory
  #39 = Utf8               (Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
  #40 = String             #41            // 123\u0001789
  #41 = Utf8               123\u0001789
  #42 = Utf8               InnerClasses
  #43 = Class              #44            // java/lang/invoke/MethodHandles$Lookup
  #44 = Utf8               java/lang/invoke/MethodHandles$Lookup
  #45 = Class              #46            // java/lang/invoke/MethodHandles
  #46 = Utf8               java/lang/invoke/MethodHandles
  #47 = Utf8               Lookup
{
  public Test();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: ldc           #7                  // String 456
         2: astore_1
         3: aload_1
         4: invokedynamic #9,  0              // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;)Ljava/lang/String;
         9: astore_2
        10: getstatic     #13                 // Field java/lang/System.out:Ljava/io/PrintStream;
        13: aload_2
        14: invokevirtual #19                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        17: return
      LineNumberTable:
        line 6: 0
        line 7: 3
        line 8: 10
        line 10: 17
}
SourceFile: "Test.java"
BootstrapMethods:
  0: #34 REF_invokeStatic java/lang/invoke/StringConcatFactory.makeConcatWithConstants:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
    Method arguments:
      #40 123\u0001789
InnerClasses:
  public static final #47= #43 of #45;    // Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles

@apollyon4
Copy link

디컴파일시에 코드를 원복하는 기능이 있다니 신기하네요.
생소한게 많아서 설명해주신 내용을 공부를 더 해봐야겠습니다.
직접 테스트도 해주시고 자세하게 설명해주셔서 많은 도움이 되었습니다.
감사합니다!

@benelog
Copy link
Author

benelog commented Aug 9, 2021

@apollyon4
더 정확히 표현하면, '코드를 원복하는 기능' 이라기보다는, 바이트코드를 그대로 옮기지 않고 원래 코드일것으로 '추정'되는 모습으로 디컴파일을 해준다고 봐야할것 같습니다. 바이트코드에 소스코드의 정보가 100%는 없으니 이 추정이 완전히 맞지 않는 경우도 있을듯합니다.

테스트해보진 않았지만, 소스에서 StringBuilder를 쓴 코드도 "+"을 썼던 소스 코드로 추정해서 역컴파일하는 경우도 디컴파일의 구현와 옵션에 따라서는 발생할수 있을듯합니다.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment