[Java] Switch와 else-if의 효율성 비교 (Switch와 else-if 중에 어떤 걸 사용해야 할까?)

1. Switch와 if-else

조건에 따라 실행을 분기해야 할 때 우리는 조건문을 사용한다. Java에서는 switch / if-else 두 조건문을 선택적으로 사용 가능하다. 보통 가독성을 기준으로 선택을 많이 하나, 효율성 기준에서 어떤 것을 선택하는 것이 좋을지 비교해보려 한다.

 

일단 switch 구분에서 Strings를 사용하는 것에 관한 공식문서를 보면,

 

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.

번역
스위치 문은 각 조건에 있는 String 개체를 String.equals 메서드를 사용하는 것과 같은 방식으로 연관된 조건과 비교한다. 그렇기에 스위치문에서의 String 개체를 비교하는 것은 대소문자를 구분한다. Java 컴파일러는 일반적으로 체인 if-then-else 구문보다  스위치 문에서 더 효율적인 바이트 코드를 생성한다. 

2. Switch

자세한 내용을 확인하기 위해 Switch 구문의 작동원리에 대해 좀 더 자세히 들여다보면, Switch 구문은 컴파일할 때 table switch 혹은 lookup switch 방식을 사용한다.

2-1. Table switch

스위치의 각 케이스 구문들이 효율적인 (각 스위치의 target 간격 들로 구성된) 테이블의 인덱스로 표현될 수 있을 때 사용된다. Switch 구분의 표현식이 유효한 인덱스 범위를 넘어갈 때에는 스위치의 default 타겟을 사용한다. 테이블을 생성할 충분한 공간이 필요하다.

[table switch 예제]

int chooseNear(int i) {
    switch (i) {
        case 0:  return  0;
        case 1:  return  1;
        case 2:  return  2;
        default: return -1;
    }
}

를 컴파일하게 되면 다음과 같다.

Method int chooseNear(int)
0   iload_1             // Push local variable 1 (argument i)
1   tableswitch 0 to 2: // tableswitch의 유효 색인은 0~2
      0: 28             // If i is 0, 28로 이동
      1: 30             // If i is 1, 30으로 이동
      2: 32             // If i is 2, 32로 이동
      default:34        // 범위를 벗어날경우 default 타겟 = 34 로 이동
28  iconst_0            // i was 0; - push int constant 0...
29  ireturn             // ...and return it
30  iconst_1            // i was 1; push int constant 1...
31  ireturn             // ...and return it
32  iconst_2            // i was 2; push int constant 2...
33  ireturn             // ...and return it
34  iconst_m1           // otherwise push int constant -1...
35  ireturn             // ...and return it

Method 1을 보면,  table switch에 유효 인덱스로 0~2가 각각 부여되어 있고, 인덱스 내에서 조회될 경우 해당 target으로, 아닐 경우 default로 설정된 target으로 이동하여 이어서 진행한다. 예를 들어 i가 0일 경우, 인덱스 0에 28로 저장되어 있기에 target = 28로 이동하게 된다. 차례로 일치여부를 확인하며 진행할 필요 없이, 일치하는 target으로 바로 점프하여 이동한다.

 

다만, 스위치 구분의 cases가 많지 않을 경우, table switch의 테이블 표현 방식은 공간적인 이유로 비효율적이게 된다. 예를 들어 케이스가 -100, 0, 100 일경우 201개의 간격을 만들어놓고 실제 타겟과 매핑된 간격은 3개뿐이라면 공간적 비효율성을 가지게 된다.

2-2. Lookup switch

이 경우, lookup switch 방식이 대신 사용 될 수 있다. lookup switch 구문은 int형의 key(각 case labels의 값)과 각 타겟들을 테이블에 짝지어 저장한다. lookup switch 방식이 실행되면, 스위치 구문의 expression의 값은 해당 테이블의 key들과 각각 비교된다. 매칭되는 키가 있으면 연결된 타겟으로 이동하여 이어서 진행하게 된다. 키가 발견되지 않는다면, default target로 계속된다.

[lookup switch 예제]

int chooseFar(int i) {
    switch (i) {
        case -100: return -1;
        case 0:    return  0;
        case 100:  return  1;
        default:   return -1;
    }
}

를 컴파일하게 되면 다음과 같다.

Method int chooseFar(int)
0   iload_1
1   lookupswitch 3:
         -100: 36
            0: 38
          100: 40
      default: 42
36  iconst_m1
37  ireturn
38  iconst_0
39  ireturn
40  iconst_1
41  ireturn
42  iconst_m1
43  ireturn

 

JVM은 lookup switch 테이블이 key를 정렬된 채로 저장하게 하기에 선형 스캔보다 효율적인 검색을 할 수 있다. 그렇더라도, lookup switch는 table switch처럼 단순히 범위 체크와 인덱스를 테이블에 넣는 것과는 다르게 키와 정확히 일치하는 항목을 찾아야 한다 

그래서 tableswitch는 공간적인 고려가 충분히 된 상황에서는 lookup switch 보다 효율적일 수 있다

 

JVM의 table switch와 lookup switch는  int 형에서만 동작한다. byte, char, short 형도 내부적으로는 int로 승격되기 때문에, 해당 타입의 조건도 int 형으로 취급받으며 컴파일된다. 예제의 chooseNear 메서드가 short 타입을 operator로 받아도, JVM은 int 형과 같은 명령을 생성한다. 다른 숫자 타입은 switch를 사용하기 위해선 int형태로 범위를 줄여줘야 한다. 실제로 switch 문 내에 float, long 등을 넣으면 컴파일 에러가 난다. String의 경우 int형태의 hashcode를 변환하여 사용한다. 

3. If

if는 각 조건문마다 순차적으로 확인하며, 부합하는 조건이 나올 경우 멈춘다. 모든 조건을 각각 확인해야 하기에 조건 개수만큼의 연산이 필요한 선형적 연산 (O(N))에 가깝고 비교 로직을 끝까지 수행해야 하며, 10개의 if-else 구문 중 마지막 조건에 걸리는 값이더라도 항상 9번의 조건을 모두 탐색해야 한다.

4. Switch와 If-else 중에 어떤 걸 사용해야 할까?

If-else : 만족하는 조건이 나올 때까지 탐색하며 조건이 늘어날수록 연산량이 늘어난다. 모든 조건을 각각 확인하기에 O(N)이라고 할 수 있다. 

Switch : 경우 테이블에서 적절 조건을 조회하여 거의 바로 점프하여 조회가 되기 때문에 O(1)에 가깝다고 할 수 있다. (점프 테이블을 구성하는 오버헤드 발생)

 

다만 조건에 부합하는 확률을 고려하여 순서를 정한다면 효율적인 로직 작성 가능하고 이 경우에는 switch 구문보다 효율적일 수 있다.

다음 예제에서 if와 switch 구문이 각각 효율적인 상황을 확인해 보자

// 다음 배열은 대부분의 연산이 첫번쨰 조건에서 마무리되기에 switch 구문보다 효율적이다.
int[] temp = {-1,0,100,100,100,...,100,100};

for (int i = 0; i<temp.length; i++)	{

	if (temp[i] == 100) {
		return "H";	
	}	else if (temp[i] == 0){
		return "Z";
	}	else if (temp[i] == 1){
		return "O";
	}	else if (temp[i] == 2){
		return "T";
	}	else if (temp[i] == 3){
		return "TH";
	}	else {
		return "M1";
	}
}

// 다음 배열은 이전 조건문에서 맨마지막 조건에 대부분 걸리기에 switch문이 효율적이다.
int[] temp = {-1,-1,-1,-1,-1,...,-1,0,100};

for (int i = 0; i<temp.length; i++)	{
    while (temp[i])	{
        case 100: return "H";
        case 0: return "Z";
        case 1: return "O";
        case 2: return "T";
        case 3: return "TH";
        default: return "M1";
    }
}

 

 

다만, 다음 예제와 같은 극단적으로 구현하는 경우는 개발환경에서 잘 나타나지 않고, 조건별 확률이 비슷하거나 예측이 안 되는 경우가 많은데, 이럴 경우 switch 구문이 효율적이다.

 

하지만, if-else문, switch 문에 조건이 굉장히 많아지는 케이스는 잘 없기에 체감이 될 정도의 차이나 문제가 발생하진 않으며, 단순 성능 외에 가독성 등을 잘 고려하여 선택적으로 사용하여야 한다.

 

 

 

 

 

 

참고

https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-3.html#jvms-3.10

https://docs.oracle.com/javase/7/docs/technotes/guides/language/strings-switch.html