※ 아래 어셈블리 구문들은 Intel 64 bit, Visual Studio 2013 Community 버전에서 컴파일되었습니다.
switch-case 구문은 C/C++에서 조건문이 상수인 경우에 사용할 수 있는 조건 분기문이다. 조건문에 변수도 사용할 수 있는 if 구문의 특수한 형태라고 할 수 있는데 대개 처음 C/C++을 배울 때 특정 조건에서 switch 구문의 우월함을 같이 접하곤 한다. 정말인지 다음 코드를 보자.
양쪽 모두 n이 3으로 시작하며 1일 때, 2일 때를 거쳐 종료하는 로직이다. 이 코드를 기본 릴리즈 환경으로 빌드하면 대개 속도 최적화가 적용된 어셈블리 코드가 나오는데 먼저 if 구문의 코드부터 살펴보자.
처음 보는 사람은 약간 복잡해보일 수 있지만 우리가 이미 원래의 코드를 알고 있으므로 더듬더듬 추적해볼 수 있을 것이다. 위 코드를 흐름 그대로 따라간다면 다음과 같다.
1. eax 레지스터에 3을 대입한다.
2. eax 레지스터와 1을 비교한다. (=eax 레지스터에서 1을 뺀다)
3. (그 결과값이 0과) 같지 않으므로 0x00BB1026 주소로 점프한다.
4. eax 레지스터와 2를 비교한다. (=eax 레지스터에서 2를 뺀다)
5. (그 결과값이 0과) 같지 않으므로 0x00BB103C 주소로 점프한다.
6. eax 레지스터와 3을 비교한다. (=eax 레지스터에서 3을 뺀다)
7. (그 결과값이 0과) 같으므로 printf("echo 3")를 수행한다.
8. eax 레지스터에 1을 대입한다.
9. 무조건 0x00BB1010 주소로 점프한다.
10. eax 레지스터와 1을 비교한다.
11. 같으므로 printf("echo 1")을 수행한다.
12. eax 레지스터에 2를 대입한다.
13. 무조건 0x00BB1010 주소로 점프한다.
14. eax 레지스터와 1을 비교한다.
15. 같지 않으므로 0x00BB1026 주소로 점프한다.
16. eax 레지스터와 2를 비교한다.
17. 같으므로 printf("echo 2")를 수행한다.
18. eax 레지스터에 4를 대입한다.
19. 무조건 0x00BB1010 주소로 점프한다.
20. eax 레지스터와 1을 비교한다.
21. 같지 않으므로 0x00BB1026 주소로 점프한다.
22. eax 레지스터와 2를 비교한다.
23. 같지 않으므로 0x00BB103C 주소로 점프한다.
24. eax 레지스터와 3을 비교한다.
25. 같지 않으므로 0x00BB1052 주소로 점프한다.
26. 끝.
점프의 횟수를 잘 봐두고 이제 다음 switch 구문의 코드를 보자.
위와 같은 방법으로 다시 한 번 추적해보자.
1. eax 레지스터에 3을 대입한다.
2. eax 레지스터에서 1을 뺀다. (2가 됨)
3. 그 결과값이 0과 같지 않으므로 점프하지 않는다.
4. eax 레지스터에서 1을 뺀다. (1이 됨)
5. 그 결과값이 0과 같지 않으므로 점프하지 않는다.
6. eax 레지스터에서 1을 뺀다. (0이 됨)
7. 그 결과값이 0과 같으므로 점프하지 않는다.
8. printf("echo 3")을 수행한다.
9. eax 레지스터에 1을 대입한다.
10. 무조건 0x00BB1070 주소로 점프한다.
11. eax 레지스터에서 1을 뺀다. (0이 됨)
12. 그 결과값이 0과 같으므로 0x00BB109B 주소로 점프한다.
13. printf("echo 1")을 수행한다.
14. eax 레지스터에 2를 대입한다.
15. 무조건 0x00BB1070 주소로 점프한다.
16. eax 레지스터에서 1을 뺀다. (1이 됨)
17. 그 결과값이 0과 같지 않으므로 점프하지 않는다.
18. eax 레지스터에서 1을 뺀다. (0이 됨)
19. 그 결과값이 0과 같으므로 0x00BB108A 주소로 점프한다.
20. printf("echo 2")를 수행한다.
21. eax 레지스터에 4를 대입한다.
22. 무조건 0x00BB1070 주소로 점프한다.
23. eax 레지스터에서 1을 뺀다. (3이 됨)
24. 그 결과값이 0과 같지 않으므로 점프하지 않는다.
25. eax 레지스터에서 1을 뺀다. (2가 됨)
26. 그 결과값이 0과 같지 않으므로 점프하지 않는다.
27. eax 레지스터에서 1을 뺀다. (1이 됨)
28. 그 결과값이 0과 같지 않으므로 0x00BB10AC 주소로 점프한다.
29. 끝.
수행되는 코드는 switch 구문이 조금 더 길어졌지만 점프 명령은 if 구문이 9개인 것에 반해 switch 구문이 6개로 1/3이나 적다. (보통 점프 구문이 많으면 성능 손실이 많아진다) 하지만 이 코드는 우리가 눈으로 보아도 최적화가 가능할 정도로 간단하면서 수행 순서의 예측도 가능한 코드이기 때문에 이견의 여지가 있다.
그럼 아래와 같은 코드는 어떨까?
이 코드들은 입력값에 따라 동작이 달라지므로 컴파일러가 임의로 수행 순서를 추측하여 최적화를 수행할 수 없다. 이 부분의 어셈블리 명령어는 다음과 같이 나오는데,
차분히 비교해보면 알겠지만 두 로직 간에는 거의 차이가 없다. if 구문은 같지 않으면 점프하고, switch 구문은 같으면 점프한다는 차이 뿐이다. 다만 if 구문의 경우는 코드의 수행이 예측 가능하건 말건 비슷한 모양새를 가진 것을 볼 수 있다.
결론적으로, 현대의 컴파일러는 일반적인 조건에서 if 구문의 최적화가 switch 구문과 맞먹을 정도로 좋아졌으며, 특정한 조건 하에서의 최적화는 여전히 switch 구문이 좋다는 것으로 요약할 수 있겠다. 그 특정한 조건은 위의 예에서와 같이 현업에서는 쉽게 볼 수 없고 눈으로도 최적화할 수 있는 경우가 많으므로, 이제는 성능을 위해 무조건 switch 구문을 써라! 같은 조언은 하지 않아도 될 것이다. (사실 '이제는' 이라는 말도 어폐가 있을만큼 if 구문의 최적화는 적용된 지 오래되었다)
-----------------------------------------------------------------------------------------
덧 : 글을 저지르고 읽다보니 여전히 이견이 있을 수 있어 내용을 추가한다. (친절함이 상당수 사라져 그림이 없다)
switch문의 경우 case의 개수가 일정 이상 커지면 지금 보는 모양과는 전혀 다른 모양을 갖는데(일명 점프 테이블이라고 한다) 대개 switch문이 if문보다 효율이 좋다고 말하는 근거에 해당한다. 기준이 되는 case의 개수는 컴파일러에 따라 다르다.
if문의 최적화 역시 마찬가지로 switch문처럼 일정 이상 커지면 점프 테이블을 구성해주기도 하는데 이 역시 컴파일러러에 의존적이다. 점프 테이블은 모르오 같은 컴파일러도 있다(안타깝게도 VS 컴파일러가 상당수 포함된다). 위에서는 jne 인스트럭션을 사용했지만 if문을 사용하더라도 switch문처럼 je 인스트럭션을 사용하도록 코드를 작성할 수도 있다(코드 유지보수상 권장하지는 않는다).
이도저도 복잡하고 잘 모르겠고 귀찮고 고민할 시간적 비용이 나오지 않는다면, 그냥 switch를 쓰는 것이 무방하다. 물론 굉장한 구형 컴파일러를 만난다면 switch 구문도 어마어마한 비효율 코드를 발생시키기도 하지만, 나처럼 이런 글까지 써놓고도 컴파일러를 크게 신뢰하지 않는 것도 하나의 방법이라 하겠다.
(좋은 시간 낭비의 사례)