안녕하세요. A.N.S.I 부원 주진형입니다.
오늘은 if문과 switch문의 차이점에 대해서 알아보도록 하겠습니다.
if문 과 switch 차이점??
흔히 알고 있는 if 문과 switch 의 차이는
if문은 하나하나 비교! switch문은 그 곳으로 바로 이동!
이렇게 움직이는 방식이 차이점 입니다.
if문 방식
#include <stdio.h>
int main() {
int a = 5;
int b = 0;
if(a == 1){
b = 3;
}
else if(a == 2){
b = 4;
}
else if(a == 5){
b = 1;
}
else{
b = 2;
}
printf("%d\n", b);
return 0;
}
- 첫번째 if 문에서 a가 1인지 비교 -> 1이면 b에 3을 넣음. 아니면 2. 로 이동.
- 두번째 if 문에서 a가 2인지 비교 -> 2이면 b에 4를 넣음. 아니면 3. 으로 이동.
- 세번째 if 문에서 a가 5인지 비교 -> 5이면 b에 1을 넣음. 아니면 4. 로 이동.
- 위의 if 문에서 하나도 참이 아니라면 b에 2를 넣음.
이러한 일련의 과정을 거쳐서 b의 값이 출력됩니다.
switch문 방식
#include <stdio.h>
int main() {
int a = 5;
int b = 0;
switch(a){
case 1:
b = 3;
break;
case 2:
b = 4;
break;
case 5:
b = 1;
break;
default:
b = 2;
break;
}
printf("%d\n", b);
return 0;
}
- switch문의 비교할 값이 a 다.
- a 값에 해당하는 case 문으로 이동. 없다면 default로 이동.
이러한 일련의 과정을 거쳐서 b의 값이 출력됩니다.
뭐가 다른거지?
if문과 switch문의 차이는 간단히 말해서
바로 실행 위치로 가는가? 아니면 하나하나 확인해서 가는가?
이 두개의 차이입니다.
조금 더 자세하게 설명을 하자면,
if문은 branch statement 로 분기를 나눠서 실행을 할지 말지 정하는 방식입니다.
그에 반해
switch문은 jump statement 로 특정한 곳으로 점프해서 그 곳을 실행하는 방식을 택합니다.
그래서 분기가 많아지면 switch문이 속도 측면에서는 더 이득을 볼 수 있습니다.
하지만 특정 위치로 jump 하기 위해서 더 많은 메모리를 사용해야하기 때문에 메모리적인 측면에서는 if문이 더 적은 메모리를 사용합니다.
자세한 설명
아래의 내용은 굳이 알 필요는 없으므로 궁금하지 않으시다면 밑으로 내려 결론을 확인 해주시면 감사하겠습니다.
아래의 디스어셈블은 visual studio 2019, 1927 버전으로 사용되었습니다. x86 기준으로 빌드 했습니다.
if문
위의 if문 코드를 visual studio 디스어셈블 기능을 이용해서 디스어셈블 해본 결과 값입니다.
if (a == 1) {
005C1846 cmp dword ptr [a],1
005C184A jne main+45h (05C1855h)
b = 3;
005C184C mov dword ptr [b],3
}
005C1853 jmp main+6Ah (05C187Ah)
else if (a == 2) {
005C1855 cmp dword ptr [a],2
005C1859 jne main+54h (05C1864h)
b = 4;
005C185B mov dword ptr [b],4
}
005C1862 jmp main+6Ah (05C187Ah)
else if (a == 5) {
005C1864 cmp dword ptr [a],5
005C1868 jne main+63h (05C1873h)
b = 1;
005C186A mov dword ptr [b],1
}
005C1871 jmp main+6Ah (05C187Ah)
else {
b = 2;
005C1873 mov dword ptr [b],2
}
printf("%d\n", b);
005C187A mov eax,dword ptr [b]
005C187D push eax
005C187E push offset string "%d\n" (05C7B30h)
005C1883 call _printf (05C1046h)
005C1888 add esp,8
위에서 설명한 대로 되어있을까요?
코드 분석
먼저 아래의 코드부터 하나씩 살펴봅시다.
if (a == 1) {
005C1846 cmp dword ptr [a],1
005C184A jne main+45h (05C1855h)
b = 3;
005C184C mov dword ptr [b],3
}
005C1853 jmp main+6Ah (05C187Ah)
-
첫번째 줄에서는 a 와 1을 비교합니다.
-
그리고 두번째 줄에서 비교한 값이 같지 않으면 05C1855h 로 jump 시킵니다. 같으면 무시되고 밑의 mov(005C184C) 으로 이동합니다.
- b에 3을 대입합니다.
- 05C187Ah 로 이동합니다.
여기서 2. 에서 이동하는 005C184C 는 어디일까요?
바로 두번째 if문입니다.
else if (a == 2) {
005C1855 cmp dword ptr [a],2
005C1859 jne main+54h (05C1864h)
b = 4;
005C185B mov dword ptr [b],4
}
005C1862 jmp main+6Ah (05C187Ah)
- 위와 동일하게 a를 2와 비교합니다.
- 같지 않으면 05C1864h 로 점프. 같으면 무시되고 3. 으로 이동합니다.
- b에 4를 대입합니다.
- 05C187Ah 로 이동합니다.
그렇다면 4. 의 05C187Ah 는 어딜까요?
printf("%d\n", b);
00007FF7D744189C mov edx,dword ptr [b]
바로 마지막의 printf 문의 위치입니다.
이렇게 if문 은 위에서부터 확인하면서
값 비교, 같으면 실행 후 if문 블럭 탈출 아니면 다음 if문 비교.
이 방식을 그대로 유지하고 있음을 알 수 있습니다.
그래서 최악의 경우에는 O(n) 의 시간복잡도를 가지는 것으로 확인할 수 있습니다.
switch문
이번에도 동일하게 switch문을 사용해봅시다.
switch (a) {
00271846 mov eax,dword ptr [a]
00271849 mov dword ptr [ebp-0DCh],eax
0027184F cmp dword ptr [ebp-0DCh],1
00271856 je main+5Ch (027186Ch)
00271858 cmp dword ptr [ebp-0DCh],2
0027185F je main+65h (0271875h)
00271861 cmp dword ptr [ebp-0DCh],5
00271868 je main+6Eh (027187Eh)
0027186A jmp main+77h (0271887h)
case 1:
b = 3;
0027186C mov dword ptr [b],3
break;
00271873 jmp main+7Eh (027188Eh)
case 2:
b = 4;
00271875 mov dword ptr [b],4
break;
0027187C jmp main+7Eh (027188Eh)
case 5:
b = 1;
0027187E mov dword ptr [b],1
break;
00271885 jmp main+7Eh (027188Eh)
default:
b = 2;
00271887 mov dword ptr [b],2
break;
}
printf("%d\n", b);
0027188E mov eax,dword ptr [b]
00271891 push eax
00271892 push offset string "%d\n" (0277B30h)
00271897 call _printf (0271046h)
0027189C add esp,8
코드 분석
switch문 시작부터 보겠습니다.
00271846 mov eax,dword ptr [a]
00271849 mov dword ptr [ebp-0DCh],eax
eax 레지스터에 a 값을 대입합니다.
그리고 [ebp-0DCh] 위치에 eax 값을 대입합니다.
즉 [ebp-0DCh] 위치에는 a 의 값이 대입되어있습니다.
0027184F cmp dword ptr [ebp-0DCh],1
00271856 je main+5Ch (027186Ch)
a를 1과 비교, 같으면 027186Ch 로 이동합니다.
00271858 cmp dword ptr [ebp-0DCh],2
0027185F je main+65h (0271875h)
a를 2와 비교, 같으면 027186Ch 로 이동.
00271861 cmp dword ptr [ebp-0DCh],5
00271868 je main+6Eh (027187Eh)
a를 5와 비교, 같으면 027186Ch 로 이동.
0027186A jmp main+77h (0271887h)
위의 jump 에 아무것도 걸리지 않으면 0271887h 로 이동.
이거 완전 if문이랑 똑같은 거 아닌가요?
다른 코드
그렇다면 이번에는 switch문에 분기가 조금 더 많으면 어떻게 될지 다시 해보겠습니다.
switch (a) {
00F41846 mov eax,dword ptr [a]
00F41849 mov dword ptr [ebp-0DCh],eax
00F4184F mov ecx,dword ptr [ebp-0DCh]
00F41855 sub ecx,1
00F41858 mov dword ptr [ebp-0DCh],ecx
00F4185E cmp dword ptr [ebp-0DCh],8
00F41865 ja $LN12+9h (0F418C5h)
00F41867 mov edx,dword ptr [ebp-0DCh]
00F4186D jmp dword ptr [edx*4+0F418F4h]
case 1:
b = 3;
00F41874 mov dword ptr [b],3
break;
00F4187B jmp $LN12+10h (0F418CCh)
case 2:
b = 4;
00F4187D mov dword ptr [b],4
break;
00F41884 jmp $LN12+10h (0F418CCh)
...(중략)
case 8:
b = 10;
00F418B3 mov dword ptr [b],0Ah
break;
00F418BA jmp $LN12+10h (0F418CCh)
case 9:
b = 11;
00F418BC mov dword ptr [b],0Bh
break;
00F418C3 jmp $LN12+10h (0F418CCh)
default:
b = 2;
00F418C5 mov dword ptr [b],2
break;
}
printf("%d\n", b);
00F418CC mov eax,dword ptr [b]
00F418CF push eax
00F418D0 push offset string "%d\n" (0F47B30h)
00F418D5 call _printf (0F41046h)
00F418DA add esp,8
달라진 어셈블리어
이번에는 조금 다릅니다.
분기가 10개정도로 늘어나니, 다른 어셈블리어가 나왔습니다.
하나하나 읽어보겠습니다.
00F41846 mov eax,dword ptr [a]
00F41849 mov dword ptr [ebp-0DCh],eax
00F4184F mov ecx,dword ptr [ebp-0DCh]
00F41855 sub ecx,1
eax 에 case 문의 개수를 집어 넣습니다. 그리고 eax를 하나 빼줍니다.
이렇게 되면 eax의 크기는 jump table (후에 설명) 와 동일해집니다.
00F41858 mov dword ptr [ebp-0DCh],ecx
00F4185E cmp dword ptr [ebp-0DCh],8
00F41865 ja $LN12+9h (0F418C5h)
switch문에 있는 a 의 값과 8을 비교합니다. (왜 8이냐면 case문에서 가장 큰 값이 9고, 9에서 1을 뺀 값이 8이기 때문입니다.)
8과 비교해서 a가 더 크다면 case 문으로 이동할 수 없기에 바로 default(0F418C5h)로 mov 해줍니다.
00F41867 mov edx,dword ptr [ebp-0DCh]
00F4186D jmp dword ptr [edx*4+0F418F4h]
이 부분이 jump table을 거쳐서 case 문으로 이동하는 곳입니다.
jump table에 맵핑 되는 edx*4+0F418F4h 이 값으로 이동해서 jump 하게 되어있습니다.
결국 위의 분기가 작을 때와 다르게 jump table을 이용해서 하나하나 비교해서 이동하는 것이 아닌, 한 번에 실행할 곳으로 이동해서 실행을 하게 됩니다.
차이점
-
위에서 볼 수 있듯, switch문은 분기의 개수가 작을 때는 (4개 미만) if 문과 동일하게 하나하나 비교해서 움직이게 됩니다.
하지만 분기의 개수가 클 때는 jump table 을 이용해서 실행하고자 하는 곳으로 바로 움직이게 됩니다.
-
switch는 바로 이동할 수 있다는 장점이 있지만, jump table을 만들어서 저장해야하기에 메모리적인 측면에서 손해를 볼 수 있습니다.
저는 정확히 모르지만 참고한 블로그의 말을 따르면
jump table은 (case문의 가장 큰 값 - case 문의 가장 작은 값 + 1)의 크기로 정해진다.
위와 같이 설명 되어있습니다.
그래서 최적화를 조금 더 하고 싶다면 오름차순이나 내림차순으로, 최대한 작은 메모리를 먹게 하면서 사용하는 것이 좋을 것으로 보입니다.
결론
switch와 if문은 비슷해 보이지만 많은 차이가 있습니다.
if문은 branch statement 기반으로 순차적으로 비교를 하고, 메모리를 덜 먹습니다.
switch문은 jump statement 기반으로 크기가 클 때는 jump table 을 참고해서 바로 jump를 하게됩니다.
이 둘은 사실 속도적인 측면으로는 큰 차이가 나지 않습니다. 일반적인 high language (c, c++ 등등) 에서는 거의 속도차이가 없을 것으로 보입니다.
그렇기에 실제로 사용할 때는 가독성과, 더 어울리는 방식을 택해서 사용해주는 것이 더 맞다고 생각듭니다.