자바 & 스프링

string concatenation compile optimization

p829911 2022. 1. 5. 00:04

전 포스팅에서 Java에서 String을 처리하는 데 사용하는 클래스들을 소개한적이 있다.

2021.12.08 - [자바 & 스프링] - java - String, StringBuilder, StringBuffer

 

java - String, StringBuilder, StringBuffer

String, StringBuilder, StringBuffer는 자바에서 문자열을 다루는 대표적인 클래스이다. String Java에서 String 객체를 생성하는 방법은 2가지가 있는데 하나는 "" 큰 따옴표를 사용하는 것이고, 두번째는 new..

p829911.tistory.com

이 포스팅에선 JDK에서 컴파일 시, 문자열 결합 연산을 어떻게 최적화 하는지 알아보자.

JAVA 버전에 따른 Compile시 String 연결 전략

String 연산은 1.4 버전까지 매우 비용이 많이 드는 연산이였다. 이후 J2SE 5.0 버전 부터 Compile 할 때 String 연산 작업을 StringBuilder 를 이용해 바이트 코드를 변경하여 최적화 해준다. Java SE 9 버전 부터는 컴파일 시점에 바이트 코드를 변경하지 않고 런타임 시점에 String을 연산하는 전략을 바꿀 수 있도록 invokedynamic을 사용하도록 변경했다.

이제 예를 들어 살펴보자.

Java8로 진행한 위의 예제를 살펴보자.
왼쪽은 실제 자바 코드이고 오른쪽은 javap로 .class 파일을 역어셈블한 결과이다.

오른쪽 사진에 빨간색 부분으로 표시한 것이 String Constant Pool에 등록된 문자열이다.

  • 변수 e에서 보이듯이 두 개의 큰따옴표로 더하기 연산을 하면 compile 시점에 합쳐져서 하나의 문자열이 된다.
  • a 변수에 할당한 것처럼 b + c + d의 문자열 객체 연산이 일어나면 StringBuilder로 바이트 코드를 조작해 compile 시점에 최적화를 해준다.

이번엔 약간 다른 예제이다. 

  • 기존에 있던 b라는 변수("Hello ")에 큰따옴표를 이용해 문자열("World")을 결합시키면 큰따옴표로 감싸진 문자열("World")은 String Pool에 저장된다. 
  • c 변수에 "World" 문자열을 저장했지만 오른쪽에 역컴파일된 코드를 보면 #6 String World로 String Pool에 먼저 등록된 "World" 라는 문자열을 가져오는 것을 볼 수 있다.

여기까지 읽으면 이런 궁금증이 생길 수 있다. Compiler가 자동으로 StringBuilder를 이용해 String 연산 최적화를 해주니 우리가 구지 코드에서 StringBuilder를 안써도 되지 않을까? 아니다. 아래의 예를 보자

1번 코드는 실제 작성한 자바 코드이다. result라는 변수에 백만번 "some data" 라는 문자열을 더하는 연산을 하는 코드를 작성했다. 이 코드를 Java Compiler가 자동으로 최적화 해주면 2번 코드가 된다. 2번 코드는 StringBuilder를 쓸데없이 백만번 생성하기 때문에 GC가 많이 작동하여 성능이 매우 안좋을 것이다. 우리는 3번 코드를 작성해야한다. 결론은 아무리 Compiler 가 자동으로 최적화를 해준다고 하더라도 Compiler만 믿고 코드를 작성하지 말아야 한다는 것이다. 또한 StringBuilder에 최종 문자열의 예상 길이를 넣어 초기화 해주는 최적화 작업은 Compiler가 해주지 못하기 때문에 문자열 연산 최적화 작업은 꼭 직접 해줘야 한다.

 

이젠 Java9 부터 바뀐 문자열 최적화 작업을 살펴보자 Java9은 invokedynamic을 사용하여 바이트코드를 변경하지 않고 연결 전략을 변경할 수 있다. 즉 클라이언트는 재컴파일 없이도 런타임시 최적화된 새로운 전략을 적용할 수 있게 되는 것이다.

다음과 같은 프로세스로 새로운 최적화 작업이 진행된다.

  1. 문자열 연결을 설명하는 함수 시그니쳐를 준비한다. (String, String) -> String
  2. 문자열 연결에 필요한 실제 인수를 준비한다. arg1: "Hello ", arg2: "World"
  3. bootstrap 메서드를 호출하고 준비한 함수 시그니쳐와 인자를 메서드에 전달한다.
    <bootstrap 메서드가 뭐지? 나중에 한번 정리해봐야겠다>
  4. 함수 시그니처에 대한 실제 구현을 생성하고 이를 MethodHandle 내부에 캡슐화한다.
  5. 생성된 함수를 호출하여 결합된 최종 문자열을 생성한다.

이제 예를 들어 살펴보자

Java8에서 봤던 예제와 같은 코드이다.

오른쪽에 역컴파일된 코드를 보면 아까 예제와 비교했을 때 보다 훨씬 간결해졌다. 실제 문자열 결합은 런타임시 문자열 결합 전략을 받아 수행된다. 그럼 문자열 결합 전략은 어떤 것들이 있을까?

  • BC_SB (ByteCode StringBuilder): 런타임 시 동일한 StringBuilder 바이트코드를 생성한다.
  • BC_SB_SIZED: StringBuilder에 필요한 용량을 추측하려고 시도한다. 그 외에는 이전 방식과 동일하다. 
  • BC_SB_SIZED_EXACT: StringBuilder를 사용하고, 필요한 용량을 정확히 계산한다. 필요한 용량을 계산하기 위해 모든 인수를 문자열로 변환한다. (int -> toString)
  • MH_SB_SIZED: MethodHandle을 기반으로 하고 StringBuilder API를 호출한다.
  • MH_SB_SIZED_EXACT: 이전과 유사하지만 StringBuilder의 용량을 정확히 계산한다.
  • MH_INLINE_SIZE_EXACT: 필요한 용량을 미리 계산하고, 전달된 인자들을 연결하여 byte 배열로 생성 후, String의 private 생성자를 이용하여 copy 없이 byte 배열을 전달하여 String 객체를 반환한다.

6가지의 전략이 있는데 이중에 default는 마지막 전략인 MH_INLINE_SIZE_EXACT이다. 이 전략을 변경하고 싶으면

-Djava.lang.invoke.stringConcat=<strategyName> 을 시스템 속성으로 지정하여 실행하면 된다.

 

참고로 아래의 예를 보면 Java8 버전에서 봤던 것과는 다르게

b라는 변수 ("Hello ")에 큰따옴표로 만든 "World"를 더해도 "World"라는 문자열은 String Pool에 저장되지 않는다. 그 이유는 String 연산이 런타임 시점에 일어나기 때문이다.

 

 

 

참고

https://medium.com/javarevisited/java-compiler-optimization-for-string-concatenation-7f5237e5e6ed

 

Java Compiler Optimization for String Concatenation

String concatenation was a costly affair in the early Sun Java versions(till JDK1.4 to be precise). Even though later JDK’s brought the…

medium.com

https://www.baeldung.com/java-string-concatenation-invoke-dynamic

https://jerry92k.tistory.com/50

 

[Java] String + 연산 최적화

JDK 5 이전 버전에서는 String + 연산시, 매 연산마다 String 객체가 생성되는 비효율이 있었습니다. String str1 = "a"; String str2 = str1+"b"+"c"; // => str1 객체 생성, str1+"b" 객체 생성, str1+"b"+"c"..

jerry92k.tistory.com

 

'자바 & 스프링' 카테고리의 다른 글

Stream 연습 문제 1 - 기본  (0) 2022.01.12
string관련 클래스 성능 비교  (0) 2022.01.06
ThreadLocal의 활용 - 트랜잭션 동기화  (0) 2021.12.22
ThreadLocal의 정의와 사용법  (0) 2021.12.20
트랜잭션  (0) 2021.12.20