Search

if~else와 switch의 차이

Created
2020/06/10
tag
C
씹어먹는 C 언어
if~else
switch

1. 들어가기 전에

if~elseswitch의 차이는 예전부터 궁금했던 것이긴 하다. 하지만 이에 대해서 더 깊게 알아볼 생각보다는 그저 if~else if~else에서 반복되는 else if를 줄이고자 switch 구문을 이용하는 줄로만 알고 있었다. 하지만 단순히 반복되는 else if를 지우기 위해서만 switch를 쓰는 것이 아닌 것을 알게 되었다. 우선 switch의 성능은 if~else보다 좋을 수도 있고 비슷할 수도 있다. switch의 성능은 들어오는 조건에 따라서 결정되는데, if~else 구문보다 성능이 더 좋을 수도 있는 것은 Assembly 코드를 통해서 이해할 수 있다.
위에서 성능이 더 좋을 수도 있다는 것은 기본적으로 switch 구문을 이용할 수 있는 것을 의미하기 때문에 조건식이 표현식이 아닌 상수로 주어질 때를 의미한다.
간단하게 설명하자면, Assembly에서의 if~else if~else의 경우 모든 조건에 대해서 cmp를 하게 된다. 반면에 switch의 경우, 특정 조건이 만족된다면 Assembly에서 일일이 모든 케이스에 대해서 cmp를 하는 것이 아니라 Jump Table을 이용할 수 있기 때문에 성능이 더 좋을 수 있는 것이다. Jump Table을 이용하는 경우에는 케이스로 들어온 값의 index에 해당하는 주소로 바로 Jump를 하기 때문에 값의 비교 없이 더 빠르게 판별이 가능한 것이다.

2. if~else

우선 위 코드는 if~else에 대해서 Assembly 코드로 바꾸어 나타낸 결과이다. 색깔에 맞춰서 각 구문들을 살펴보면 매 조건 검사마다 cmp를 수행하는 것을 알 수 있다.

3. switch

if~else의 결과에 유념하여 switchAssembly 코드로 확인해보자. switch를 확인할 때는 몇 가지 경우로 나누어 확인해보고자 한다.
연속되어 있는 수들에 대해서 0부터 시작할 때
연속되어 있는 수들에 대해서 0부터 시작하지 않을 때
연속되어 있는 수들에 대해서 정렬을 하지 않았을 때
불규칙한 수들을 이용할 때

1) 연속되어 있는 수들에 대해서 0부터 시작할 때

왼쪽을 보면 value에 대한 case 들이 0~5까지 연속된 수들로 나타난 것을 볼 수 있다. 좌측에서의 switch 라인은 7번이고, 우측에서 좌측의 7번 라인에 해당하는 부분은 11번~17번 라인이다. 여기서 나타나는 우측의 11번~17번 라인이 Jump Table이다.
그럼 switch를 이용하면 Jump Table의 어디로 Jump를 해야하는지 어떻게 알 수 있을까? Jump Table의 몇 번째를 이용해야 하는지는 우측의 5번~10번 라인을 보면 알 수 있듯이 rax를 계산하여 알 수 있다, 5번~10번라인은 좌측의 5번 라인에 해당하는데 case로 들어오는 값이 할당되는 순간 이 rax라는 인덱스가 계산 되는 것을 알 수 있다.
즉, 연속된 수에 대해서는 모든 조건에 대해서 cmp를 이용하지 않고도 어느 곳으로 향해야 하는지 알 수 있게 되고 해당 블록으로 이동하여 케이스에 맞는 행동들을 수행하게 되는 것이다.
연속된 수라고 하더라도 특정 개수 이상이 연속되지 않으면 일일이 cmp를 하기도 하는 것을 보아 컴파일러에 따라서 달라지는 것 같다.

2) 연속되어 있는 수들에 대해서 0부터 시작하지 않을 때

현재 5번~12번 라인을 보면 이전 5번~10번 라인 부분에 비해 2줄 더 긴것을 알 수 있다.
이번 예시를 살펴보면 첫 번째 예시와 크게 다르지 않다. 0부터 시작하던 케이스가 7부터 시작하게 된 것일 뿐이다. 이 때 우측의 Assembly 코드를 확인해보자. 13번~19번 라인을 보면 알 수 있듯, 여전히 Jump Table을 이용하는 것은 동일하다. 하지만 Jump Table을 이용하기 위해서, 케이스에 들어오는 값에 맞춰 rax 인덱스를 계산하는 부분의 라인이 이전 예시에 비해서 2줄 더 긴 것을 볼 수 있다.
가장 눈에 띄는 차이는 sub 명령어가 추가된 것이다. 해당 구문을 확인해보면 케이스에 들어오는 값을 0부터 시작하기 위해 케이스의 가장 작은 값 7을 케이스에 들어오는 value에서 빼게 된다. 따라서 좌측에서의 케이스는 7부터 시작하여 12까지에 대해서 확인을 하지만, 실제로 Assembly 코드에서는 이를 정규화하여 0에 맞춰서 Jump Table을 이용하도록 케이스를 확인하게 된다. 이는 Jump TableIndex를 0부터 이용하도록 하기 위해서이다.
따라서 연속된 수더라도 그 값이 0부터 시작하지 않으면, 인덱스를 0부터 이용할 수 있도록 맞춰주기 위해 별도의 sub 연산을 수행하기 때문에 sub 연산 없이 switch를 이용할 수 있도록 하는 것이 좋다.

3) 연속되어 있는 수들에 대해서 정렬을 하지 않았을 때

우선 정렬되어 있는 경우는 첫 번째 예시와 두 번째 예시를 보면 된다. 정렬이 되어 있지 않은 예시는 바로 위 사진과 같다. 케이스에 들어가는 값들도 7~12로 바로 이전 예시와 동일하고 Jump Table 역시 이전과 동일하다. 우측의 13번 라인을 살펴보면 인덱스 0으로 들어가는 값에 대해서는 L9, 인덱스 1로 들어가는 값은 L8, ... , 인덱스 5로 들어가는 값에 대해서는 L3로 이동하게 된다. Jump Table 생성까지는 이전 예시와 동일하지만, 각 케이스와 이동할 곳의 매칭이 다르다.
이전 예시에서는 순차적으로 정렬이 되어 있었기 때문에 Jump Table에 할당된 순서대로 각 케이스에 매칭하면 되었지만, 현재 예시에서는 순차적으로 정렬이 되어 있지 않기 때문에 Jump Table에 할당된 순서대로 케이스에 매칭하면 안 된다. 따라서 각 케이스에 맞는 인덱스에 따라 Jump Table을 이용할 수 있도록 따로 이동할 곳의 순서를 조정하여 매칭해야 한다.
따라서 케이스를 순차적으로 정렬을 하여 이용하는 것이 정렬을 하지 않고 이용하는 것보다 더 낫다.

4) 불규칙한 수들을 이용할 때

어느 정도 연속적인 수들을 이용할 때

7~11까지는 연속한 수를 주고 그 이후는 55라는 차이가 조금 큰 수를 케이스로 넣었다. 이런 경우는 정렬되어 있는 연속된 수를 이용할 때와 사뭇 비슷하다. 7~11에 대해서는 (0에서 시작한 것이 아니기 때문엔 인덱스를 맞추기 위해 7을 빼는 과정이 있다.) 5개의 인덱스를 가진 Jump Table을 이용할 수 있도록 되어 있다. 하지만 Jump Table을 애초에 이용하기 이전에, 불규칙한 수에 대해서 switch 문에서 검사를 하게 된다. (이전 예시들에서는 switch는 곧바로 Jump Table로 향했다.) 따라서 어느 정도 연속적인 수를 사용하게 되면, 불규칙한 수에 대해서 switch에서 먼저 판별 하고, 나머지 수들에 대해서 Jump Table을 이용하게 된다.
11~55까지의 차이에 대해서는 Jump Table을 만들지 않지만 11~50까지의 차이에 대해서는 11~50까지의 수는 이용하지 않음에도 Jump Table을 두는 것을 보아, 컴파일러에 따라서 Jump Table을 만들지 말지에 대한 판단이 다를 것 같다.

꽤 불규칙한 수들을 이용할 때

대체적으로 불규칙한 수들을 이용하게 되면 큰 수들을 역순으로 비교하고 몇 개의 작은 수들에 대해서는 순서대로 비교를 하게 된다.
현재 예시에 대해서는 뒤의 4개에 대해서는 큰 수부터 역순으로 비교, 앞의 2개에 대해서는 작은 수부터 순서대로 비교를 하는 것을 보아 컴파일러에 따라서 다를 것 같다.
즉, 모든 수들에 대해서 cmp 연산을 하게 된다.

5) 결론

크게 4개의 경우를 통해 switch를 살펴보니, 정렬되어 있는 상태에서 0부터 시작하는 값을 연속하여 사용하는 것이 if~else보다 더 낫고, 정렬이 되어 있지 않은 상태에서 0부터 시작하지 않고 연속되지 않는 불규칙한 값을 사용하게 되면 if~else와 같이 모든 케이스에 대해 cmp를 하게 되므로 if~else와 비슷한 성능을 띄게 된다는 것을 유추할 수 있었다.

4. Reference