if-else, switch 성능비교

Updated:

최근에 회사 팀원분의 추천으로 ‘크리에이티브 프로그래머’라는 책을 읽어보았는데, 비판적 사고에 대한 중요성을 강조하고 있다.

비판적 사고란 정보를 받아들일 때 단순히 수용하지 않고, 의심하고 분석하는 과정을 말한다. 보통 새로운 기술을 강의나 책을통해서 배울때면, 두 기술을 단순 비교하고 상황에 따라서 적합한 기술은 무엇인지 알려주는 경우가 많은데, 유명한 책에서 그렇다니까 ‘왜’라는 질문 없이 그렇구나 하고 넘어가고는 했다.

지금부터라도 그냥 넘어갔던, 어쩌면 당연하다고 생각했던 것들에 대해서 의심해보고 직접 확인해보는 시간을 가져보고자 한다. 가장 먼저 자바를 처음 배웠을때 무심코 넘어갔던 if-else문과 switch문의 성능 차이에 대해서 알아보도록 하자.

if-else, switch 바이트코드 분석

간단한 분기문을 if-else문과 switch문으로 작성하였으며 1, 3, 5에 해당하는 case값을 설정하였다.

public int ifElseStatement() {  
    int temp = 0;
    int num = generateRandomIntFromOneToTen();
  
    if (num == 1) {  
        temp = 1;  
    } else if (num == 3) {  
        temp = 3;  
    } else if (num == 5) {  
        temp = 5;  
    } else {  
        temp = 99;  
    }  
    return temp;  
}
public int switchStatement() {
    int temp = 0;
	int num = generateRandomIntFromOneToTen();
	
    switch (num) {
        case 1:
            temp = 1;
            break;
        case 3:
            temp = 3;
            break;
        case 5:
            temp = 5;
            break;
        default:
            temp = 99;
    }

    return temp;
}

작성한 두 분기문이 내부적으로 어떻게 동작하는지 자바 바이트코드를 통해 분석해보았다.

public ifElseStatement()I
    ICONST_0
    ISTORE 1
    ALOAD 0
    GETFIELD example/IfElseVSSwitchTest.num : Ljava/lang/Integer;
    INVOKEVIRTUAL java/lang/Integer.intValue ()I
    ICONST_1
    IF_ICMPNE L6
    ICONST_1
    ISTORE 1
    GOTO L8
   
   //...
   
   L9
    ALOAD 0
    GETFIELD example/IfElseVSSwitchTest.num : Ljava/lang/Integer;
    INVOKEVIRTUAL java/lang/Integer.intValue ()I
    ICONST_3
    IF_ICMPNE L8
    ICONST_3
    ISTORE 1
   L8
    ICONST_4
    ISTORE 1
    ILOAD 1
    IRETURN

if-else문은 연속된 if와 else if 문을 통해 차례대로 조건을 확인한다. 바이트코드에서는 이를 IF_ICMPNE 명령어를 통해 구현하였으며 두 값을 비교하고 결과에 따라 분기하고 있다.

public switchStatement()I
	L0
	LINENUMBER 43 L0
	ICONST_0
	ISTORE 1
	L1
	LINENUMBER 46 L1
	ALOAD 0
	INVOKEVIRTUAL example/IfElseVSSwitchTest.generateRandomIntFromOneToTen ()V
	L2
	LINENUMBER 48 L2
	ALOAD 0
	GETFIELD example/IfElseVSSwitchTest.num : Ljava/lang/Integer;
	INVOKEVIRTUAL java/lang/Integer.intValue ()I
	TABLESWITCH
	  1: L3
	  2: L4
	  3: L5
	  4: L4
	  5: L6
	  default: L4
	L3
	LINENUMBER 50 L3
	FRAME SAME
	ICONST_1
	ISTORE 1
	GOTO L7
	L5
	LINENUMBER 53 L5
	FRAME SAME
	ICONST_2
	ISTORE 1
	GOTO L7
	L6
	LINENUMBER 56 L6
	FRAME SAME
	ICONST_
	ISTORE 1
	GOTO L7
	L4
	LINENUMBER 59 L4
	FRAME SAME
	BIPUSH 99
	ISTORE 1
	L7
	LINENUMBER 63 L7
	FRAME SAME
	ILOAD 1
	IRETURN
	L8
	LOCALVARIABLE this Lexample/IfElseVSSwitchTest; L0 L8 0LOCALVARIABLE temp I L1 L8 1
	MAXSTACK = 2
	MAXLOCALS = 2

switch문의 바이트코드를 보면 TableSwitch라는게 보인다. TableSwitch는 정수 값에 기반한 switch문에 사용되며, TableSwitch 뒤에 나오는 1: L3, 3: L5, 5: L6은 각 case에 대한 정보를 나타낸다.

예를 들어, 1: L3num이 1일 경우 L3레이블로 점프하라는 의미이며 default: L9 또한 num이 1, 3, 5중 어느값도 아니라면 L9라벨로 점프하라는 의미이다.

L3, L5, L6 라벨들은 각각의 case에 대한 코드블록을 나타내며 정수 1을 스택에 푸시하고, 스택의 최상위 값을 temp 변수에 저장하는것을 나타낸다. 그 다음 GOTO L7에서 제어를 L7 레이블로 이동시킨다.

그런데 한가지 이상한 부분이 있다. 우리는 1, 3, 5에 해당하는 case만 줬지만 case들 사이의 값인 2, 4또한 default에 해당하는 control flow를 따르도록 컴파일 된 것을 확인할 수 있다.

이는 TABLESWITCH의 특징으로, TABLESWITCH는 case 범위 내 모든 값을 인덱싱하여 jump table을 구성하고, 해당하는 인덱스의 실행지점으로 바로 점프한다. 별도의 비교연산 없이 바로 이동할 수 있기 때문에 TABLESWITCH를 활용한 switch문은 O(1)시간이 소요된다.

하지만 TABLESWITCH가 모든 상황에 적합한건 아니다. TABLESWITCH는 case의 모든 범위를 인덱싱하기 때문에 case의 수가 적더라도 각 case의 범위가 넓다면 (1, 10, 100) 1 ~ 100까지의 정수를 모두 인덱싱하게 되어 과도한 jump table 생성 비용과 공간 비용이 발생한다.

이러한 비용을 줄이기 위해서 컴파일러는 case의 범위가 넓은 경우에 LOOKUPTABLE 방식을 선택하여 switch문을 구현한다.

LOOKUPSWITCH
  1: L3
  10: L4
  100: L5
  default: L6

위의 바이트코드에서 볼 수 있듯, LOOKUPTABLE은 case값 만으로 테이블을 구성한다.

인덱스를 이용해 바로 찾아가는게 아니기 때문에 LOOKUPTABLE는 비교대상변수와 table에 구성된 key와의 비교연산이 필요한데, 시간적 비용을 보완하고자 key들을 정렬하여 구성해놓고 바이너리 서치 알고리즘과 같은 것을 적용하여 O(logN) 시간을 보장하게 된다.

결론적으로 if-else문은 분기 개수(N)에 따라 최악의 경우 O(N)이 소요되며, switch문은 분기 개수(N)에 따라 최악의 경우에도 O(logN) 시간 수행된다는 것을 알 수 있었다.

이제 분석을 넘어서 실제로 switch문이 if-else문보다 더 나은 처리 속도를 보여주는지 검증해보도록 하자.

성능 비교

둘의 성능을 비교 측정하기위해 JMH를 사용하였으며 다음과 같은 시나리오에서 테스트를 진행해보았다.

jmh {  
    threads = 1  
    fork = 1  
    warmupIterations = 1  
    iterations = 1  
}
  • if-else와 switch문 (TABLESWITCH)의 성능차이
  • if-else와 switch문 (LOOKUPTABLE)의 성능차이

    if-else와 switch문 (TABLESWITCH)의 성능차이

@State(Scope.Benchmark)  
@BenchmarkMode(Mode.AverageTime)  
@OutputTimeUnit(TimeUnit.MICROSECONDS)  
public class IfElseVSSwitchTest {  
  
    private Integer num;  
  
    @Setup  
    public void generateRandomIntFromOneToTen() {  
        Random random = new Random();  
        this.num = random.nextInt(10) + 1;  
    }  
  
    @Benchmark  
    public int ifElseStatement() {  
        int temp = 0;  
  
        for (int i = 0; i < 5_000; ++i) {  
            generateRandomIntFromOneToTen();  
  
            if (num == 1) {  
                temp = 1;  
            } else if (num == 3) {  
                temp = 3;  
            } else if (num == 5) {  
                temp = 5;  
            } else {  
                temp = 99;  
            }  
        }  
        return temp;  
    }  
  
    @Benchmark  
    public int switchStatement() {  
        int temp = 0;  
  
        for (int i = 0; i < 5_000; ++i) {  
            generateRandomIntFromOneToTen();  
  
            switch (num) {  
                case 1:  
                    temp = 1;  
                    break;  
                case 3:  
                    temp = 3;  
                    break;  
                case 5:  
                    temp = 5;  
                    break;  
                default:  
                    temp = 99;  
                    break;  
            }  
        }  
        return temp;  
    }  
}

TABLESWITCH (case: 1, 3, 5) table

실행 결과 if-else문은 0.19ms, TABLESWITCH를 사용한 switch문은 0.18ms 소요되는걸 확인할 수 있었다.

if-else와 switch문 (LOOKUPSWITCH)의 성능차이

case의 범위를 넓혀서 LOOKUPSWITCH를 활용한 switch문과의 성능비교를 해보았다.

LOOKUPSWITCH (case: 1, 10, 100) lookup

실행 결과 if-else문은 0.19ms, LOOKUPSWITCH를 사용한 switch문은 0.21ms 소요되는걸 확인할 수 있었다.

마무리

지금까지 if-else문과 switch문의 성능을 비교해보았다. 이러한 성능 차이는 대부분의 경우에 있어서 실제 애플리케이션의 전체 성능에 큰 영향을 미치지는 않는다. 하지만 비판적 사고를 갖고 당연한 것에 의문을 갖는 과정 자체가 의미있었다고 생각한다. 개인적으로 가독성 측면에서 switch문을 사용하는게 좀 더 직관적이라 생각하며 다양한 요소를 고려하여 if-else 또는 switch를 선택하는 것이 어떨까 싶다.

참고자료

Categories:

Updated:

Leave a comment