티스토리 뷰

안녕하세요. Mobile Application 팀 전계원입니다.

 

G마켓 앱에는 Firebase Analytics 가 연결되어 있어서, 앱 크래시 이슈가 발생하면 Firebase Analytics 를 통해 확인해 볼 수 있습니다.

그리고 Firebase 를 통해 보고된 버그를 분석하고 수정하는 과정에서 "Java 에서 String 으로 switch 를 사용하는 것이 if-then-else 로 작성할 때 보다 성능적으로 더욱 이점을 띤다"는 사실을 알게 되었습니다.

 

단순하게 보았을 땐 switch 와 if-then-else 가 동일한 로직일 것 같은데 어떻게 성능적으로 더 이점을 띄는지 궁금해졌습니다.

 

본 글을 통해 바이트코드를 직접 읽어보며 switch 가 더욱 효과적인 이유를 알아가보고, kotlin 의 when 에서도 동일한 효과를 가지는지, 다른 자료형에서 동일하게 성능적인 이점을 가지는지 분석해본 과정을 함께 공유드리고자 합니다.



 

0. 프롤로그 - 아니 글쎄 저는 String.hashCode() 를 사용한 적이 없다니깐요?


어느 날 Firebase Analytics 를 통해 보고된 리포트를 확인하던 중 다음과 같은 이슈를 발견하게 됩니다.

 

<Firebase Analytics StackTrace Information>

해당 에러를 보고 저는 단순히 "어떤 java 코드에서 null check 없이 hashCode() 함수를 사용했겠구나" 하는 생각을 하며 코드를 확인해 보았습니다.

 

    public boolean myFunc(String url, Intent intent) {

        Uri uri = Uri.parse(url);

>>      switch (uri.getScheme()) {

그런데 오류가 발생한 코드 위치를 확인해 보니.. hashCode() 라는 함수는 보이지 않았습니다.

hashCode() 함수를 사용하지 않았는데 어떻게 hashCode() 함수 실행 중 NullPointerException 오류가 발생할 수 있었을까요??

그 이유는 Uri.getScheme() 함수의 반환형은 String 이고, switch 문에서 string 을 인자로 받는 경우 내부적으로 hashCode() 가 실행되기 때문입니다.

 

그래서 getScheme() 의 결과가 null 이었을 때 switch 에서 null 을 가지고 hashCode() 함수를 실행하려다가 NullPointerException 이 발생한 것이었습니다.

 

uri.getScheme() 가 nullable 한 String 을 반환한다는 것은 Uri.getScheme() 함수가 그렇게 설계된 것이기에 이해할 수 있었지만,

switch 에서는 hashCode() 를 사용한다는 사실은 이해가 가지 않았습니다.

 

 

왜 switch 에서는 내부적으로 hashCode() 라는 함수가 사용되었던 것일까요?



 

1. String in switch - JDK 7


ref :&nbsp;https://docs.oracle.com/javase/8/docs/technotes/guides/language/strings-switch.html

The switch statement compares the String object in its expression with the expressions associated with each case label as if it were using the String.equals method;
consequently, the comparison of String objects in switch statements is case sensitive.
The Java compiler generates generally more efficient bytecode from switch statements that use String objects than from chained if-then-else statements.

(번역)
switch 구문은 표현식에서의 String 객체를 각 case 라벨이 있는 표현식에서의 String 을 String.equals 를 함수를 사용하여 비교합니다.
결과적으로 switch 구문에서의 String 객체를 비교하는 것은 대소문자를 구분하여 비교합니다.
Java 컴파일러는 String object 를 사용하는 switch 구문에서 일반적으로 if-then-else 구문을 사용할 때 보다 더욱 효과적인 bytecode 를 생성합니다.

"String in switch Statements" Oracle 문서를 확인해 보면, JDK 7 이 출시되면서 switch 구문에서 String object 를 표현식으로 사용할 수 있다는 내용이 보입니다.

그리고 마지막 줄에 "Java 컴파일러는 String object 를 사용하는 switch 구문에서 일반적으로 if-then-else 구문을 사용할 때 보다 더욱 효과적인 bytecode 를 생성합니다." 라는 내용을 확인해 볼 수 있습니다..

 

 

여기서 "효과적인 bytecode 를 생성한다" 라는 문장은 구체적으로 무엇을 의미하는 것일까요??



 

2. String in switch/when - bytecode


1) bytecode 란

bytecode 는 Java 컴파일러가 프로그램을 실행시키는 과정에서 자바가상머신(JVM) 이 이해할 수 있는 언어로 변환된 소스코드를 의미합니다.

 

디컴파일된 .class file

bytecode 의 확장자는 ".class" 이며, Java 코드를 컴파일하였을 때 ".class" 로 나타나는 소스코드를 통해 확인할 수 있습니다.

* (IntelliJ IDE 기준) Java bytecode 는 "View → Show Bytecode" 를 통해 bytecode 를 확인할 수 있습니다.



2) 'if-then-else vs. switch' in String

bytecode에 대해 알았으니 if-then-else 에서의 bytecode 와 switch 에서의 bytecode 를 비교해 보겠습니다.

 

참고로 테스트를 위해 사용하고 있는 java 버전은 openjdk-18 버전입니다.

 

 

동일한 기능일 것 같았던 두 코드가 바이트코드 상으로는 다른 것을 확인해 볼 수 있었습니다.

바이트 코드를 함께 읽어보며 구체적인 동작과정을 알아보겠습니다.



3) bytecode 이해하기 (if-then-else)

LINENUMBER 3 L0 : 실제 Java 코드 3번째 줄에 있는 내용이 L0 영역의 내용임을 의미합니다. (무시해도 됩니다)

LDC "string2" : 정적변수가 모여있는 영역에서 "string2" 라는 문자열을 가져와서 stack 에 push 해준다는 의미입니다.

ASTORE 1 : stack top 에 있는 "string2" 를 1번 저장소에 저장한다는 의미입니다.

 

 

ALOAD 1 : 1번 저장소에서 값을 가져와서 stack 에 push 하는 것을 의미합니다.

LDC "string1" : 정적변수가 모여있는 영역에서 "string1" 라는 문자열을 가져와서 stack 에 push 해준다는 의미입니다.

INVOKEVIRTUAL java/lang/String.equals (Ljava/lang/Object;)Z : 간단하게 말하면 String.equals() 를 실행하여 결괏값을 stack 에 push 해준다는 의미이며 구체적으로는 인자로 포함된 내용에 알맞은 JVM 내의 가상함수를 실행시킨다는 의미입니다.

=> String.equals() 는 stack 에 push 되어있는 2개의 값 ("string1" 과 "string2") 를 순차적으로 pop 하여 인자로 가져온 다음에 equals() 로직을 통해 두 값을 비교한 결괏값을을 다시 stack 에 push 합니다.

IFEQ L2 : stack top 의 값이 0 이면 L2 를 실행시킨다는 의미입니다.

=> equals() 의 결과가 0이었다면, equals() 의 결과가 false 였다는 의미이며 이는 2개의 값이 다른 문자열이라는 의미입니다.

=> string1 과 string2 는 일치하지 않기 때문에 0을 반환할 것이고, 바이트코드를 따라 L2 로 이동할 것입니다.

=> 만약 같다면 L1 바로 하단에 있는 L3 로 이동할 것입니다.

여기서 두 문자열이 같다고 가정하고 L3 의 내용을 확인해 보겠습니다.

 

 

GETSTATIC java/lang/System.out : Ljava/io/PrintStream 은 정적 field 값을 가져온다는 의미입니다. PrintStream 이라는 정적 값을 가져옵니다.

LDC "string1" : "string1" 을 stack 에 push 합니다.

INVOKEVIRTUAL... : PrintStream.println() 함수를 실행합니다. 이때 println() 함수 내에서 stack 의 top 인 "string1" 을 pop 하여 인자값으로 가져옵니다. 즉, System.out.println("string1") 이 실행됩니다.

GOTO L4 : 바로 L4 영역으로 이동하는 코드입니다.

 

 

그리고 L4 영역은 "RETURN" 즉, 현재의 함수를 종료하는 의미를 담고 있습니다.

 

 

        // decompile if-then-else
        String testString = "string2";
        if (testString.equals("string1")) {
            System.out.println("string1");
        } else if (testString.equals("string2")) {
            System.out.println("string2");
        } else {
            System.out.println("else string");
        }

위와 같은 bytecode 분석내용을 바탕으로 바이트코드 내용을 다시 java 코드로 작성해보면

처음 작성하였던 코드와 크게 다르지 않다는 것을 알 수 있습니다.

 

 

4) switch 로 생성된 bytecode 이해하기

이번엔 switch 문의 bytecode 를 읽어보겠습니다.

 

 

LDC "string2" : 정적변수가 모여있는 영역에서 "string2" 라는 문자열을 가져와서 stack 에 push 함을 의미합니다.

ASTORE 1 : stack top 에 있는 "string2" 를 pop 한 뒤 1번 저장소에 저장합니다.

L0 의 내용은 if-then-else 의 코드와 동일하였습니다.

 

 

ALOAD 1 : 1번 저장소에 저장된 "string2" 를 stack 에 push 합니다.

ASTORE 2 : stack top 에 있는 "string2" 를 pop 하여 2번 저장소에 저장합니다.

ICONST_M1 : -1 을 int 형으로 가져와서 stack top 에 push 한다는 의미입니다.

ISTORE 3 : stack top 에 있는 -1 을 pop 하여 3번 저장소에 저장합니다.

=> 참고로 저장하는 자료형이 reference 형이면 A로 시작하고 (ASTORE, ALOAD), int 형이면 I로 시작합니다 (ISTORE, ILOAD)

ALOAD 2 : 2번 저장소에 저장되어 있던 "string2" 를 stack 에 push 합니다.

INVOKEVIRTUAL : String.hashCode() 를 실행합니다.

=> 이때 stack top 에 있던 "string2" 가 pop 되어, String.hashCode() 함수의 인자로 들어갑니다.

=> 그리고 hashCode() 의 결괏값을 stack 으로 push 합니다.

LOOKUPSWITCH : 이를 통해 stack top 에 있는 값을 pop 하여 가져온 뒤 이를 key 로 삼아서 알맞은 위치의 영역으로 이동합니다.

=> stack top 에서 가져온 값이 -1881759168 이면 L2 로 이동하고, -1881759167 이면 L3 로 이동하고, default 이면 L4 로 이동합니다.

 

 

-1881759167 에 해당하는 L3 로 이동했다면

ALOAD 2 : 2번 저장소에 저장되어 있던 "string2" 를 stack 에 push 합니다.

LDC "string2" : "string2"도 을 stack 에 push 합니다

INVOKEVIRTUAL : String.equals() 를 실행시키고 인자로 stack 의 top 에서 2개의 값을 (string2, string2) 를 pop 한 뒤 인자로 넣어줍니다.

=> 두 string 은 같은 문자열이기에 결괏값은 true 인 1일 것이고, 이를 stack top 으로 push 합니다.

IFEQ L4 : 0 이면 L4 로 이동합니다.

=> 1 이기에 L4 로 바로 이동하지 않습니다. 그렇기에 하단에 있는 ICONST_0 으로 이어집니다.

ICONST_0 : 0 을 가져와서 stack 에 push 합니다.

ISTORE 3 : stack 의 top 인 0 을 pop 한 뒤 3번 저장소에 저장합니다.

 

 

그다음 코드영역인 L4 에서는

ILOAD 3 : 3번 저장소에 있는 값을 stack 에 push 합니다.

LOOKPSWITCH : 3번 저장소에 있던 값이 0인지 1인지 그 외의 값인지에 따라 L5, L6, L7 로 분기하고 있으며, L5, L6, L7 은 각각 "string1", "string2", "else string" 을 출력하는 코드영역입니다.

 

 

    // decompile switch
    public static void main(String args[]) {
        String testString = "string2";
        switch (testString.hashCode()) {
            case -1881759168:
                if (testString.equals("string1"))
                    System.out.println("string1");
                else
                    System.out.println("else string");
                break;
            case -1881759167:
                if (testString.equals("string2"))
                    System.out.println("string2");
                else
                    System.out.println("else string");
                break;
            default:
                System.out.println("else string");
                break;
        }
    }

위와 같은 내용을 기반으로 bytecode 의 내용을 다시 java 코드로 변경하면

위와 같이 변경될 수 있다는 점을 알 수 있습니다.

 

 

5) when 으로 생성된 bytecode 이해하기

kotlin 의 when 구문에서도 bytecode 가 어떻게 변경되는지 알아보겠습니다.

 

fun main() {
    val testString = "string2"
    when(testString) {
        "string1" -> println("string1")
        "string2" -> println("string2")
        else -> println("else string")
    }
}

Kotlin 또한 디버그 과정에서 bytecode 로 JVM 에 전달되기 때문에 when 구문으로 작성 후 bytecode를 확인해 보겠습니다.

* (IntelliJ IDE 기준) Kotlin 은 코드 작성 후 "Tools > Kotlin > Show Kotlin Bytecode" 를 통해 bytecode 를 확인할 수 있습니다.

 

 

bytecode 로 비교해 보니 Java 에서의 LOOKUPSWITCH 가 Kotlin 에서는 TABLESWITCH 인 점을 제외하면 전체적인 로직은 많이 유사하다는 것을 알 수 있습니다.

이를 통해 Kotlin 의 when 도 switch 문처럼 최적화 로직이 적용되어 있음을 알 수 있었습니다.



6) 정리

bytecode 상으로 읽었던 내용을 정리하면 다음과 같습니다.

  • if-then-else조건문들을 하나하나 비교하며 분기처리 작업을 수행합니다
  • switchLOOKUPSWITCH 를 통해 분기처리 작업을 수행합니다
  • whenTABLESWITCH 를 통해 분기처리 작업을 수행합니다



 

3. LOOKUPSWITCH 와 TABLESWITCH


그렇다면 이젠 LOOKUPSWITCH 와 TABLESWITCH 을 사용한 분기처리는 무엇이 다른지 궁금해집니다.

 

 

1) LOOKUPSWITCH

ref : https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html#jvms-6.5.lookupswitch

[LOOKUPSWITCH]

Operation
Access jump table by key match and jump

Description
A lookupswitch is a variable-length instruction.
Immediately after the lookupswitch opcode, between zero and three bytes must act as padding, such that defaultbyte1 begins at an address that is a multiple of four bytes from the start of the current method (the opcode of its first instruction).
Immediately after the padding follow a series of signed 32-bit values: default, npairs, and then npairs pairs of signed 32-bit values. The npairs must be greater than or equal to 0.
Each of the npairs pairs consists of an int match and a signed 32-bit offset. Each of these signed 32-bit values is constructed from four unsigned bytes as (byte1 << 24) | (byte2 << 16) | (byte3 << 8) | byte4.

The table match-offset pairs of the lookupswitch instruction must be sorted in increasing numerical order by match.

The key must be of type int and is popped from the operand stack. The key is compared against the match values.
If it is equal to one of them, then a target address is calculated by adding the corresponding offset to the address of the opcode of this lookupswitch instruction.
If the key does not match any of the match values, the target address is calculated by adding default to the address of the opcode of this lookupswitch instruction.
Execution then continues at the target address.

The target address that can be calculated from the offset of each match-offset pair, as well as the one calculated from default, must be the address of an opcode of an instruction within the method that contains this lookupswitch instruction.

Notes
The alignment required of the 4-byte operands of the lookupswitch instruction guarantees 4-byte alignment of those operands if and only if the method that contains the lookupswitch is positioned on a 4-byte boundary.
The match-offset pairs are sorted to support lookup routines that are quicker than linear search.

----

(번역)

역할
key 값을 기반으로 jump table 에 접근하며, jump 합니다.

설명
lookupswitch 는 가변길이 실행구문입니다.
lookupswitch opcode 직후의 0~3 byte 는 padding 의 역할을 합니다, 이는 defaultbyte1 는 현재 함수의 시작 부분(해당 실행단위의 첫 번째 opcode 부분) 으로부터 4 배수에 있는 주소값 이후부터 시작된다는 것을 의미합니다. 
padding 직후에는 signed 32bit 값(기본값, npairs 및 signed 32bit 의 npair 쌍) 이 이어집니다. 이때의 npair 들은 0보다 크거나 같습니다.
각 npair 쌍들은 int 형의 match 값과 signed 32bit offset 으로 이루어져 있습니다. signed 32bit 값은 4개의 unsigned byte 로 이루어져 있습니다.

lookupswitch 실행구문의 match-offset 쌍으로 이루어진 table 은 반드시 match 값을 기준으로 오름차순으로 정렬되어 있습니다.

key 값은 반드시 int 형이어야 하고 이는 operand stack 에서 pop 하여 전달받습니다. key 는 match 값들과 비교합니다.
만약 key 값이 match 값들 중 하나에 속하면, target 주소값은 LookupSwitch 실행구문의 opcode 주소값에서 offset 값을 추가하여 계산됩니다.
만약 key 값이 match 값들 중 어떤 것에도 속하지 않으면, target 주소값은 LookupSwitch 실행구문의 opcode 주소값에서 default 에 해당하는 offset 값을 추가하여 계산됩니다.
그리고 해당 target 주소값으로 이동합니다.

default offset 과 match-offset 쌍의 offset 으로부터 계산된 target 주소값은 반드시 lookupswitch 실행구문에서 포함하고 있는 실행함수들이 가지고 있는 opcode 의 주소값이어야 합니다.

비고
4byte operand의 lookupswitch 명령에 필요한 정렬은 lookupswitch를 포함하는 method가 4byte 경계에 위치해 있는 경우에만 해당 operand의 4byte 정렬을 보장합니다.
match-offset 쌍은 lookup routine 이 선형탐색보다 빠르게 동작되기 위하여 정렬되어 있습니다.

 

정리하자면 

 

    LOOKUPSWITCH
      -1881759168: L2
      -1881759167: L3
      default: L4

위 LOOKUPSWITCH 코드를 예시로 보았을 때

lookupswitch 이후에는 가변길이의 인자(여러 개의 npairs)가 추가될 수 있으며,

npairs 은 match 값(-1881759168, -1881759167) 과 offset(L2, L3) 값으로 이루어져 있고, 이들은 match 값을 기준으로 오름차순으로 정렬되어 있습니다.

 

오름차순으로 정렬된 까닭은 operand stack 에서 pop 을 통해 가져온 값이 match 값들 중 일치하는 것이 있는지 선형탐색보다 빠르게 탐색하기 위함이며, 선형탐색의 시간복잡도인 O(N) 보다 빠르면서 정렬을 필요로 한다는 점으로 미루어보았을 때 공식문서에 기재되어있진 않지만 이진탐색(binary-search)을 통해 match 값들을 탐색할 것으로 추측해 볼 수 있습니다.

 

 

2) TABLESWITCH

ref : https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html#jvms-6.5.tableswitch

[TABLESWITCH]

Operation
Access jump table by index and jump

Description
A tableswitch is a variable-length instruction.
Immediately after the tableswitch opcode, between zero and three bytes must act as padding, such that defaultbyte1 begins at an address that is a multiple of four bytes from the start of the current method (the opcode of its first instruction).
Immediately after the padding are bytes constituting three signed 32-bit values: default, low, and high.
Immediately following are bytes constituting a series of high - low + 1 signed 32-bit offsets.
The value low must be less than or equal to high.
The high - low + 1 signed 32-bit offsets are treated as a 0-based jump table.
Each of these signed 32-bit values is constructed as (byte1 << 24) | (byte2 << 16) | (byte3 << 8) | byte4.

The index must be of type int and is popped from the operand stack.
If index is less than low or index is greater than high, then a target address is calculated by adding default to the address of the opcode of this tableswitch instruction.
Otherwise, the offset at position index - low of the jump table is extracted.
The target address is calculated by adding that offset to the address of the opcode of this tableswitch instruction. Execution then continues at the target address.

The target address that can be calculated from each jump table offset, as well as the one that can be calculated from default, must be the address of an opcode of an instruction within the method that contains this tableswitch instruction.

Notes
The alignment required of the 4-byte operands of the tableswitch instruction guarantees 4-byte alignment of those operands if and only if the method that contains the tableswitch starts on a 4-byte boundary.



----

(번역)

역할
index 값을 기반으로 jump table 에 접근하며, jump 합니다.


설명
tableswitch 는 가변길이 실행구문입니다.
tableswitch opcode 직후의  0~3 byte 는 padding 의 역할을 합니다, 이는 defaultbyte1 는 현재 함수의 시작 부분(해당 실행단위의 첫 번째 opcode 부분) 으로부터 4 배수에 있는 주소값 이후부터 시작된다는 것을 의미합니다.  
padding 직후에는 3개의 signed 32bit 값(default, low, high)으로 구성되어 있습니다.
그리고 그 이후에는 signed 32bit 의 (high - low + 1) offset 들로 구성되어 있습니다.
low 값은 high 값 보다 작거나 같습니다.
sign 32bit 의 (high - low + 1) offset 값들은 0-based jump table 로 다루어집니다.
이 signed 32bit 값들은 4개의 byte 값으로 이루어져 있습니다.

index 값은 반드시 int 형이어야 하고 이는 operand stack 에서 pop 하여 전달받습니다.
index 가 low 값 보다 작거나, high 값 보다 크면 target address 는 tableswitch 실행구문의 opcode 주소값으로부터 default 에 해당하는 값을 더하여 계산됩니다.
그렇지 않다면, offset 은 jump table 에서 (index - low) 을 통해 추출됩니다.

target address 는 각 jump table offset 을 기반으로 계산되며, 이는 default 값일 수도 있다. 그리고 이들의 주소값에 해당하는 opcode 는 반드시 tableswitch 실행구문에서 포함하고 있는 실행함수들이 가지고 있는 opcode 의 주소값이어야 합니다.


비고
4byte operand의 tableswitch 명령에 필요한 정렬은 tableswitch를 포함하는 method가 4byte 경계에 위치해 있는 경우에만 해당 operand의 4byte 정렬을 보장합니다.

 

정리하자면

 

    TABLESWITCH
      -1881759168: L3
      -1881759167: L4
      default: L5

위 TABLESWITCH 코드를 예시로 보았을 때

tableswitch 이후에는 가변길이의 인자(여러 개의 npairs)가 추가될 수 있으며, 그 이후에는 default, low, high 값이 있고, 그 이후에는 signed 32bit 의 offset(L2, L3) 값들로 이루어져 있습니다.

 

여기서 operand stack 의 top 으로부터 index 값이 들어오면 low, high 값을 기반으로 알맞은 offset 값을 바로 계산하여 다음에 실행할 실행구문의 주소값을 알아냅니다.

이때 'index - low 를 기반으로 offset 을 추출하는 방법'array 혹은 map 에서 index 에 해당하는 값을 조회할 때 사용하는 방법과 유사하며, 결국 O(1) 의 시간복잡도로 offset 값을 가져올 수 있음을 알 수 있습니다.



3) if-then-else vs switch vs when

총 정리하면 다음과 같습니다.

 

if-then-else : 모든 조건문에 대해 true 인 조건이 나올 때까지 하나하나 비교합니다
=> O(N)

 

switch (lookupswitch) : 모든 조건을 signed 32bit 의 정수로 치환 및 오름차순 정렬 후 이진탐색으로 조건과 일치하는 위치를 찾아서 해당 코드로 jump 합니다

=> O(logN)

 

when (tableswitch) : 모든 조건을 index 로 분류하고, 조건으로 들어온 index 값에 해당하는 실행구문의 주소값을 low, high 값을 기반으로 찾아서 해당 코드로 jump 합니다

=> O(1)

 

위와 같은 차이로 인하여 분기처리가 길어질 수록 if-then-else 대신 switch/when 을 사용하는 것이 bytecode 로 변환 후 JVM 위에서 동작할 때 더욱 효율적이라는 것을 알 수 있었습니다.



 

4. String 이 아닌 다른 자료형에서는 어떻게 변환될까?


그렇다면 String 뿐만 아니라 다른 자료형에서는 어떻게 bytecode가 구성이 될까요??

 

 

ref :&nbsp; https://docs.oracle.com/javase/tutorial/java/nutsandbolts/switch.html

switch 에서는 byte, short, char, int 와 같은 primitive 타입Enum, String 그리고 Character, Byte, Short, Integer 과 같은 Wrapper type 만을 활용할 수 있습니다.

그래서 String 을 제외한 나머지 자료형 중 int 와 enum 자료형을 보면서 바이트코드가 어떻게 변환되는지 직접 확인해 보겠습니다.

 

 

int 형은 switch 문과 when 모두 추가되는 로직이 없었습니다.

int 이기 때문에 String 처럼 hashCode() 해줄 필요 없었고, String 때와 동일하게 각각 LOOKUPSWITCH 와 TABLESWITCH 를 사용하여 분기처리하고 있었습니다.

 

 

enum 은 ordinal 값을 기반으로 분기처리하는 로직이 추가되어 있었습니다.

결론적으로 switch 와 when 모두 어떤 자료형에서나 이들을 표현할 수 있는 고유 정수값으로 변환된 후에 LOOKUPSWITCH 와 TABLESWITCH 을 통해 알맞은 코드영역으로 jump 하도록 변환되는 것을 알 수 있었으며,

 

그렇기에 다른 자료형에서도 분기가 길어지면 if-then-else 구문보다 switch/when 이 더욱 효율적이라는 점을 알 수 있었습니다.



 

5. TL;DR


 분기 처리가 길어질 경우 switch/when 구문에서 string 을 사용하는 것이 if-then-else 사용보다 더욱 효율적입니다. 그 이유는 if-then-else 와 다르게 switch/when 는 바이트코드상에서 각각 LOOKUPSWITCH / TABLESWITCH 실행구문을 통해 작업을 최적화하기 때문입니다.

 

 lookupswitch 는 조건을 signed 32bit 값으로 변환 및 오름차순으로 정렬한 뒤에 이진탐색으로 key 값에 대응하는 실행주소값을 찾아서 jump 하는 방식이며, tableswitch 는 조건을 index 기반으로 나열 후 index 값에 대응하는 실행주소값을 찾아서 jump 하는 방식으로 동작합니다. 그래서 if-then-else 를 통해 일치하는 조건을 선형탐색하는 것보다 효율적으로 동작할 수 있습니다.

 

 String 에서 hashCode() 를 사용하는 것은 String 를 고유숫자로 변환하여 LOOKUPSWITCH/TABLESWITCH 에 활용하기 위함이며, 다른 자료형들도 모두 각자를 표현할 수 있는 고유의 숫자로 전환되어 switch 에서는 LOOKUPSWITCH, when 에서는 TABLESWITCH 를 통해 개선된 시간복잡도로 조건에 알맞은 실행코드로 jump 하는 형태로 bytecode 가 생성되고 있었습니다.



 

6. 참고자료


- string in switch (oracle) : https://docs.oracle.com/javase/8/docs/technotes/guides/language/strings-switch.html

- bytecode instructions : https://en.wikipedia.org/wiki/List_of_Java_bytecode_instructions

- jvm Instruction set (oracle) : https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html

- The switch Statement (oracle) : https://docs.oracle.com/javase/tutorial/java/nutsandbolts/switch.html

댓글