개요
if-else 와 switch문은 성능 상 약간의 차이가 있습니다. 오늘은 그 구조를 이해해보고 언제 사용해야 좋을지 바이트코드를 알아보도록 하겠습니다.
if
int i = 1;
if (i == 1) {
method1();
} else if (i == 3) {
method2();
} else {
method3();
}
int i의 값이 1일때 method1()을 실행하고, i의 값이 3일때 method2()를 실행하고, 나머지는 method3()을 실행하는 아주 간단한 구조를 가진 if 문입니다. 이 코드를 bytecode로 변환해보겠습니다.
public static main([Ljava/lang/String;)V
L0
LINENUMBER 7 L0
ICONST_1
ISTORE 1
L1
LINENUMBER 9 L1
ILOAD 1
ICONST_1
IF_ICMPNE L2
L3
LINENUMBER 10 L3
INVOKESTATIC functional/Main.method1 ()V
GOTO L4
L2
LINENUMBER 11 L2
FRAME APPEND [I]
ILOAD 1
ICONST_3
IF_ICMPNE L5
L6
LINENUMBER 12 L6
INVOKESTATIC functional/Main.method2 ()V
GOTO L4
L5
LINENUMBER 14 L5
FRAME SAME
INVOKESTATIC functional/Main.method3 ()V
L4
LINENUMBER 17 L4
FRAME SAME
RETURN
L7
LOCALVARIABLE args [Ljava/lang/String; L0 L7 0
LOCALVARIABLE i I L1 L7 1
MAXSTACK = 2
MAXLOCALS = 2
구분을 좀 나누면 이렇게 됩니다.
구조를 잘 보시면
- L1에서 조건이 일치하면 L3으로 이동하여 method1을 실행하고, 일치하지 않으면 IF_ICMPNE L2 로 이동합니다.
- L2에서 조건이 일치하면 L6으로 이동하여 method2를 실행하고, 일치하지 않으면 IF_ICMPNE L5 로 이동합니다.
- else 에서 method3을 실행합니다.
if 문은 결과가 true 가 나오기전까지 순서대로 비교연산을 수행해야합니다. 만약 비교문이 10개라면 최대 10번 비교연산이 수행될 수 있단는 것입니다.
switch
int i = 1;
switch (i) {
case 1 -> method1();
case 3 -> method2();
default -> method3();
}
public static main([Ljava/lang/String;)V
L0
LINENUMBER 7 L0
ICONST_1
ISTORE 1
L1
LINENUMBER 9 L1
ILOAD 1
LOOKUPSWITCH
1: L2
3: L3
default: L4
L2
LINENUMBER 10 L2
FRAME APPEND [I]
INVOKESTATIC functional/Main.method1 ()V
GOTO L5
L3
LINENUMBER 11 L3
FRAME SAME
INVOKESTATIC functional/Main.method2 ()V
GOTO L5
L4
LINENUMBER 12 L4
FRAME SAME
INVOKESTATIC functional/Main.method3 ()V
L5
LINENUMBER 15 L5
FRAME SAME
RETURN
L6
LOCALVARIABLE args [Ljava/lang/String; L0 L6 0
LOCALVARIABLE i I L1 L6 1
MAXSTACK = 1
MAXLOCALS = 2
switch 문을 바이트코드로 변환했을 때, 이 부분에서 switch 문이 발생된걸 확인할 수 있습니다. 위에 if-else 문과 구조가 다른데요 눈여겨 볼 것은 LOOKUPSWITCH 입니다.
LOOKUPSWITCH 는 조건마다 비교연산을 하는게 아니고 표현식 값을 가지고 탐색합니다.
if-else 보다 비교연산 없이 탐색하기 때문에 효율적이지만, jump table 생성 비용의 오버헤드가 있습니다.
LOOPUPSWITCH, TABLESWITCH 에 대해서 조금 더 Alaboza
TABLESWITCH
int i = 1;
int j = switch (i) {
case 0 -> 0;
case 1 -> 1;
case 2 -> 2;
case 5 -> 5;
case 10 -> 10;
default -> -1;
};
이 switch문을 바이트 코드로 바꾸면 어떻게 될까?
L1
LINENUMBER 9 L1
ILOAD 1
TABLESWITCH
0: L2
1: L3
2: L4
3: L5
4: L5
5: L6
6: L5
7: L5
8: L5
9: L5
10: L7
default: L5
우리는 0, 1, 2, 5, 10 에대한 case를 작성했는데 바이트 코드를 보면 TABLESWITCH 안에 0 - 10까지의 데이터가 들어가있습니다. 0, 1, 2, 5, 10을 제외한 나머지 숫자들은 switch 종료 라인인 L5 를 가리키고 있습니다.
TABLESWITCH는 전체 범위를 인덱싱 하기 때문인데요. 이 과정에서 사용되지 않은 빈 공간까지 차지하게 되어버렸습니다.
LOOKUPSWITCH와 TABLESWITCH는 컴파일과정에서 효율성을 따져서 자동으로 이 방식이 선택됩니다.
만약 인덱싱과정이 사용된다면 더더욱 switch문을 써야겠죠?
그래서 뭐가 더 좋을까?
코드를 작성할 때, 성능도 중요하지만 가독성에 대한 것도 고려해야합니다. JAVA 17부터 switch 문이 업데이트되면서 기존의 if-else 보다 가독성이 좋아졌습니다.
// switch 문
switch (i) {
case 1:
method1();
break;
case 3:
method2();
break;
default:
method3();
break;
}
// JAVA 17 에서의 switch 문
switch (i) {
case 1 -> method1();
case 3 -> method2();
default -> method3();
}
switch (i) { // 다수 조건식
case 1,2 -> method1();
case 3 -> method2();
default -> method3();
}
// JAVA 21 에서의 switch 문
switch (animal) { // 타입 캐스팅
case Cat c -> c.sound();
case Dog d -> d.sound();
case Cow c -> c.sound();
default -> "Unknown";
};
switch (animal) { // Null 처리
case Cat c -> c.sound();
case Dog d -> d.sound();
case Cow c -> c.sound();
case null -> "Null";
default -> "Unknown";
};
따라서, 가독성에 중점을 두거나 조건이 많을때는 switch 문을 사용하는것도 고려해볼만 합니다.
'Language > JAVA' 카테고리의 다른 글
[JAVA] String + 연산을 왜 쓰지말아야할까 (0) | 2024.09.24 |
---|---|
[JAVA] for문과 향상된 for문은 어떤 차이가 있을까? (0) | 2024.09.24 |
[JAVA] StringBuilder, StringBuffer 의 차이점과 주의사항 (0) | 2024.09.23 |
[JAVA] String의 불변성(immutability) (0) | 2024.09.23 |
[JAVA] 함수형 인터페이스에 대해서 알아보자 (0) | 2024.09.15 |