Note
해당 글에서는 switch-case에 대해서 분기가 어떻게 동작하는지에 대해서 알아본다.
(if-else와 최적화 방식이 같다)
구체적으로 다음과 같은 상황을 살펴볼 것이다.
분기가 3개 이하일 경우
분기가 3개를 넘을 경우 (4개 이상)
분기가 연속적이지 않을 경우
테스트 환경은 마지막 부분을 참조, MSVC에서는 분기 6개 부터 테이블로 처리한다고한다.
분기가 3개 이하일 경우
이 경우에는 직관적으로 생각되는 조건 확인 후, 다음 조건을 확인하는 식으로 동작한다. 다음 코드를 통해서 보다 구체적으로 알아보자.
#include <stdint.h>
__attribute__((noinline)) // Prevent inlining
int test(int choice) {
int ret;
switch (choice) {
case 1: {
volatile int temp_val_case1 = 100;
ret = temp_val_case1 + 5;
break;
}
case 2: {
volatile int temp_val_case2 = 200;
ret = temp_val_case2 + 10;
break;
}
case 3: {
volatile int temp_val_case3 = 300;
ret = temp_val_case3 + 15;
break;
}
default: {
volatile int temp_val_default = -1;
ret = temp_val_default;
break;
}
}
return ret;
} test:
3418089c: cmp r0, #2
220 int test(int choice) {
3418089e: sub sp, #16
222 switch (choice) {
341808a0: beq.n 0x341808c4 <test+40>
341808a2: cmp r0, #3
341808a4: beq.n 0x341808b6 <test+26>
341808a6: cmp r0, #1
341808a8: bne.n 0x341808d0 <test+52>
224 volatile int temp_val_case1 = 100;
341808aa: movs r3, #100 @ 0x64
341808ac: str r3, [sp, #0]
225 ret = temp_val_case1 + 5;
341808ae: ldr r0, [sp, #0]
341808b0: adds r0, #5
245 }
341808b2: add sp, #16
341808b4: bx lr
234 volatile int temp_val_case3 = 300;
341808b6: mov.w r3, #300 @ 0x12c
341808ba: str r3, [sp, #8]
235 ret = temp_val_case3 + 15;
341808bc: ldr r0, [sp, #8]
341808be: adds r0, #15
245 }
341808c0: add sp, #16
341808c2: bx lr
229 volatile int temp_val_case2 = 200;
341808c4: movs r3, #200 @ 0xc8
341808c6: str r3, [sp, #4]
230 ret = temp_val_case2 + 10;
341808c8: ldr r0, [sp, #4]
341808ca: adds r0, #10
245 }
341808cc: add sp, #16
341808ce: bx lr
239 volatile int temp_val_default = -1;
341808d0: mov.w r3, #4294967295
341808d4: str r3, [sp, #12]
240 ret = temp_val_default;
341808d6: ldr r0, [sp, #12]
245 }
341808d8: add sp, #16
341808da: bx lr어셈블리 코드를 확인해보면 switch-case를 if-else 체이닝으로 처리된 것을 볼 수 있다.
구체적인 해설로는
3418089c: cmp r0, #2: ‘r0’와 값 ‘2’를 비교341808a0: beq.n 0x341808c4 <test+40>: 만약 ‘choice == 2’(Branch if Equal)라면 ‘0x341808c4’로 점프341808a2: cmp r0, #3: 만약 ‘2’가 아니라면, ‘3’과 비교341808a4: beq.n 0x341808b6 <test+26>: 만약 ‘choice == 3’라면 ‘0x341808b6’로 점프341808a6: cmp r0, #1: 만약 ‘2’ 또는 ‘3’이 아니라면 ‘1’과 비교341808a8: bne.n 0x341808d0 <test+52>: 만약 ‘choice != 1’(Branch if Not Equal)라면 ‘0x341808d0’로 점프. 이 주소의 코드는 ‘default case’를 의미한다.- 만약
choice == 1이라면(즉, 이전에 어떤 분기로도 가지 않았다면), 나머지 코드를 그대로 수행한다. (‘case 1’처리 코드)
결론적으로, 분기가 3개 이하라면 if-else를 사용한 것 처럼 처리된다는 것을 알 수 있다.
분기가 3개를 넘을 경우 (4개 이상)
이 경우, 점프 테이블을 생성하여 ’case’에 해당하는 값을 인덱스로 참조하고, 그에 맞는 처리를 수행한다.
아래 코드를 통해 이를 좀 더 구체적으로 살펴보자.
#include <stdint.h>
__attribute__((noinline)) // Prevent inlining
int test(int choice) {
int ret;
switch (choice) {
case 1: {
volatile int temp_val_case1 = 100;
ret = temp_val_case1 + 5;
break;
}
case 2: {
volatile int temp_val_case2 = 200;
ret = temp_val_case2 + 10;
break;
}
case 3: {
volatile int temp_val_case3 = 300;
ret = temp_val_case3 + 15;
break;
}
case 4: {
volatile int temp_val_case4 = 400;
ret = temp_val_case4 + 20;
break;
}
default: {
volatile int temp_val_default = -1;
ret = temp_val_default;
break;
}
}
return ret;
} test:
3418089c: subs r0, #1
220 int test(int choice) {
3418089e: sub sp, #24
222 switch (choice) {
341808a0: cmp r0, #3
341808a2: bhi.n 0x341808e0 <test+68>
341808a4: tbb [pc, r0]
341808a8: asrs r6, r2, #32
341808aa: lsls r1, r1, #8
239 volatile int temp_val_case4 = 400;
341808ac: mov.w r3, #400 @ 0x190
341808b0: str r3, [sp, #16]
240 ret = temp_val_case4 + 20;
341808b2: ldr r0, [sp, #16]
341808b4: adds r0, #20
250 }
341808b6: add sp, #24
341808b8: bx lr
234 volatile int temp_val_case3 = 300;
341808ba: mov.w r3, #300 @ 0x12c
341808be: str r3, [sp, #12]
235 ret = temp_val_case3 + 15;
341808c0: ldr r0, [sp, #12]
341808c2: adds r0, #15
250 }
341808c4: add sp, #24
341808c6: bx lr
229 volatile int temp_val_case2 = 200;
341808c8: movs r3, #200 @ 0xc8
341808ca: str r3, [sp, #8]
230 ret = temp_val_case2 + 10;
341808cc: ldr r0, [sp, #8]
341808ce: adds r0, #10
250 }
341808d0: add sp, #24
341808d2: bx lr
224 volatile int temp_val_case1 = 100;
341808d4: movs r3, #100 @ 0x64
341808d6: str r3, [sp, #4]
225 ret = temp_val_case1 + 5;
341808d8: ldr r0, [sp, #4]
341808da: adds r0, #5
250 }
341808dc: add sp, #24
341808de: bx lr
244 volatile int temp_val_default = -1;
341808e0: mov.w r3, #4294967295
341808e4: str r3, [sp, #20]
245 ret = temp_val_default;
341808e6: ldr r0, [sp, #20]
249 return ret;
341808e8: b.n 0x341808b6 <test+26>
341808ea: nop 코드를 살펴보면 이전 처럼 if-else 체이닝 구조로 동작하지 않고 테이블을 통해서 점프하는 구조를 가진다는 것을 알 수 있다.
물론 최초에 ‘인덱스’를 벗어나는지에 대한 cmp를 수행하긴 하지만 나머지 부분에서는 ‘인덱스’를 기반으로 테이블에 따라서 점프하게 된다. (마지막 부분에서 해당 검사조차 피하는 법을 가볍게 살펴본다)
이제 어셈블리 코드를 보다 구체적으로 해설해보겠다.
예시 코드 어셈블리 해석
Function Prologue:
-
3418089c: subs r0, #1:- 예시 코드를 보면 case가 ‘1’부터 시작하는 것을 볼 수 있다. 따라서 인덱스 기반으로 처리하기 이전에 ‘r0 - 1’ 즉, ‘choice - 1’을 수행한다. 이를 통해서 ‘0~3’의 인덱스를 가질 수 있게된다.
이러한 방식은 다른 모든 케이스에서도 마찬가지로 동작한다. 예를 들어, case가 10부터 시작한다면 컴파일러는 ‘10’을 빼서 똑같이 0부터 인덱스가 시작되도록 조정한다.
- 예시 코드를 보면 case가 ‘1’부터 시작하는 것을 볼 수 있다. 따라서 인덱스 기반으로 처리하기 이전에 ‘r0 - 1’ 즉, ‘choice - 1’을 수행한다. 이를 통해서 ‘0~3’의 인덱스를 가질 수 있게된다.
-
3418089e: sub sp, #24:- 변수에 사용할 스택을 할당한다.
Switch Statement Implementation (Jump Table):
-
인덱스 체크 & Default Case 분기:
341808a0: cmp r0, #3: ‘r0’(choice - 1)과 3을 비교341808a2: bhi.n 0x341808e0 <test+68>:- ‘bhi.n’ (Branch if Higher, unsigned) 명령은 조건에서 첫 인자가 두 번째보다 크다면 분기한다. 현재 케이스에서는 ‘r0 > 3’이라면 ‘0x341808e0’으로 분기한다.
이제 남은 케이스는 0~3 사이에 존재한다는 것을 알 수 있다.
- ‘bhi.n’ (Branch if Higher, unsigned) 명령은 조건에서 첫 인자가 두 번째보다 크다면 분기한다. 현재 케이스에서는 ‘r0 > 3’이라면 ‘0x341808e0’으로 분기한다.
-
테이블 분기 지시
341808a4: tbb [pc, r0]:-
‘tbb’ (Table Branch Byte)은 점프 테이블을 구현하는 Thumb-2 명령어이다.
-
해당 명령은 테이블에서 바이트 단위 오프셋을 읽고, base address는 PC이며, 해당 명령어 실행 시점에서 PC + 4를 하게된다. (파이프라인과 연관이 있다는데, 아직은 잘 모르겠다.)
-
이때, ‘r0’은 해당 테이블의 인덱스로 사용된다.
-
여기서 점프 주소의 계산은 다음과 같이 이루어진다.
Thumb 명령어는 2-byte 단위로 정렬되는데, 이를 보장하기 위해서 2를 곱하는듯 하다.
-
-
점프 테이블 (명령어 x)
-
341808a8: asrs r6, r2, #32 -
341808aa: lsls r1, r1, #8 -
디버거에서는 이를 명령어로 해석했지만, 이는 tbb명령어에서 사용하는 데이터이다.
-
메모리 ‘0x341808a8’를 직접확인해보면 다음과 같다.

-
점프 테이블의 바이트는 다음과 같다.
- ‘0x341808a8’: B0 (choice-1 == 0, 즉 choice == 1)
- ‘0x341808a9’: B1 (choice-1 == 1, 즉 choice == 2)
- ‘0x341808aa’: B2 (choice-1 == 2, 즉 choice == 3)
- ‘0x341808ab’: B3 (choice-1 == 3, 즉 choice == 4)
테이블 베이스 주소(0x341808a8) 기준, 실제 오프셋을 계산식은 다음과 같다:
- choice = 1 (인덱스 0)의 분기 대상: ‘0x341808d4’ (case 1 코드)
- 필요 오프셋: (0x341808d4 − 0x341808a8) / 2 = 0x2C / 2 = 0x16
- choice = 2 (인덱스 1)의 분기 대상: ‘0x341808c8’ (case 2 코드)
- 필요 오프셋: (0x341808c8 − 0x341808a8) / 2 = 0x20 / 2 = 0x10
- choice = 3 (인덱스 2)의 분기 대상: ‘0x341808ba’ (case 3 코드)
- 필요 오프셋: (0x341808ba − 0x341808a8) / 2 = 0x12 / 2 = 0x09
- choice = 4 (인덱스 3)의 분기 대상: ‘0x341808ac’ (case 4 코드)
- 필요 오프셋: (0x341808ac − 0x341808a8) / 2 = 0x04 / 2 = 0x02
이처럼, 계산식에 따라 점프 테이블 바이트
0x16, 0x10, 0x09, 0x02가 어떻게 사용되는지 알 수 있다.이제 이렇게 구한 인덱스로 실제 주소를 예시를 통해 검증해보겠다.
예를 들어
choice == 1이라고 하면 다음과 같은 테이블을 얻을 것이다.choice = 1 ↓ subs r0, #1 ↓ r0 = 0 테이블[0] = 0x16이를 이전에 찾은 계산식에 넣으면 다음과 같은 주소를 얻는다.
0x341808a8 + (0x16 × 2) = 0x341808a8 + 0x2C = 0x341808d4코드에서
0x341808d4은 정확히case 1을 가리키는 것을 확인할 수 있다.224 volatile int temp_val_case1 = 100; 341808d4: movs r3, #100 @ 0x64 341808d6: str r3, [sp, #4] 225 ret = temp_val_case1 + 5; 341808d8: ldr r0, [sp, #4] 341808da: adds r0, #5 -
즉, 먼저 범위를 벗어나는지 확인하고, 이후에는 분기 주소의 가산분을 더해 점프하도록 동작한다.
단순하게 주소 배열을 가지고 그 만큼 점프하여 실행된다고 이해해도 된다.
분기가 연속적이지 않을 경우
이제 완전히 비연속적으로 보이는 케이스에 대해서 알아보자. 이 경우에는 위에서 살펴본 두 가지 종류가 섞여서 발생하게 된다. 바로 예제 코드를 살펴보자.
아래에 예제 코드에서 choice가 1, 147일 경우와 나머지가 어떻게 처리되는지를 위주로 살펴보자.
#include <stdint.h>
__attribute__((noinline)) // Prevent inlining
int test(int choice) {
int ret;
switch (choice) {
case 1: {
volatile int temp_val_case1 = 100;
ret = temp_val_case1 + 5;
break;
}
case 23: {
volatile int temp_val_case2 = 200;
ret = temp_val_case2 + 10;
break;
}
case 53: {
volatile int temp_val_case3 = 300;
ret = temp_val_case3 + 15;
break;
}
case 47: {
volatile int temp_val_case4 = 400;
ret = temp_val_case4 + 20;
break;
}
case 147: {
volatile int temp_val_case4 = 400;
ret = temp_val_case4 + 20;
break;
}
case 37: {
volatile int temp_val_case4 = 400;
ret = temp_val_case4 + 20;
break;
}
default: {
volatile int temp_val_default = -1;
ret = temp_val_default;
break;
}
}
return ret;
} test:
3418089c: cmp r0, #53 @ 0x35
220 int test(int choice) {
3418089e: sub sp, #32
222 switch (choice) {
341808a0: bgt.n 0x341808e0 <test+68>
341808a2: cmp r0, #22
341808a4: ble.n 0x341808d0 <test+52>
341808a6: subs r0, #23
341808a8: cmp r0, #30
341808aa: bhi.n 0x341808f2 <test+86>
341808ac: tbb [pc, r0]
341808b0: movs r1, #39 @ 0x27
341808b2: movs r1, #33 @ 0x21
341808b4: movs r1, #33 @ 0x21
341808b6: movs r1, #33 @ 0x21
341808b8: movs r1, #33 @ 0x21
341808ba: movs r1, #33 @ 0x21
341808bc: movs r1, #33 @ 0x21
341808be: movs r1, #45 @ 0x2d
341808c0: movs r1, #33 @ 0x21
341808c2: movs r1, #33 @ 0x21
341808c4: movs r1, #33 @ 0x21
341808c6: movs r1, #33 @ 0x21
341808c8: movs r1, #52 @ 0x34
341808ca: movs r1, #33 @ 0x21
341808cc: movs r1, #33 @ 0x21
341808ce: movs r3, r7
222 switch (choice) {
341808d0: cmp r0, #1
341808d2: bne.n 0x341808f2 <test+86>
224 volatile int temp_val_case1 = 100;
341808d4: movs r3, #100 @ 0x64
341808d6: str r3, [sp, #4]
225 ret = temp_val_case1 + 5;
341808d8: ldr r0, [sp, #4]
341808da: adds r0, #5
260 }
341808dc: add sp, #32
341808de: bx lr
222 switch (choice) {
341808e0: cmp r0, #147 @ 0x93
341808e2: bne.n 0x341808f2 <test+86>
244 volatile int temp_val_case4 = 400;
341808e4: mov.w r3, #400 @ 0x190
341808e8: str r3, [sp, #20]
245 ret = temp_val_case4 + 20;
341808ea: ldr r0, [sp, #20]
341808ec: adds r0, #20
260 }
341808ee: add sp, #32
341808f0: bx lr
254 volatile int temp_val_default = -1;
341808f2: mov.w r3, #4294967295
341808f6: str r3, [sp, #28]
255 ret = temp_val_default;
341808f8: ldr r0, [sp, #28]
341808fa: add sp, #32
341808fc: bx lr
229 volatile int temp_val_case2 = 200;
341808fe: movs r3, #200 @ 0xc8
34180900: str r3, [sp, #8]
230 ret = temp_val_case2 + 10;
34180902: ldr r0, [sp, #8]
34180904: adds r0, #10
260 }
34180906: add sp, #32
34180908: bx lr
249 volatile int temp_val_case4 = 400;
3418090a: mov.w r3, #400 @ 0x190
3418090e: str r3, [sp, #24]
250 ret = temp_val_case4 + 20;
34180910: ldr r0, [sp, #24]
34180912: adds r0, #20
260 }
34180914: add sp, #32
34180916: bx lr
239 volatile int temp_val_case4 = 400;
34180918: mov.w r3, #400 @ 0x190
3418091c: str r3, [sp, #16]
240 ret = temp_val_case4 + 20;
3418091e: ldr r0, [sp, #16]
34180920: adds r0, #20
260 }
34180922: add sp, #32
34180924: bx lr
234 volatile int temp_val_case3 = 300;
34180926: mov.w r3, #300 @ 0x12c
3418092a: str r3, [sp, #12]
235 ret = temp_val_case3 + 15;
3418092c: ldr r0, [sp, #12]
3418092e: adds r0, #15
260 }
34180930: add sp, #32
34180932: bx lr1$$, 의 경우 if-else 체이닝으로 처리하고 나머지 부분을 테이블로 만들어 처리하는 것을 알 수 있다.
즉, 일부는 if-else로 일부 연속적인 것은 테이블로 처리한다.
그렇다면 ‘테이블’로 처리하는 것과 if-else로 처리하는 것 간에는 어떤 기준이 적용되어 나누어지는걸까? 이는 컴파일러 혹은 최적화 옵션에 따라서 나누어진다.
구체적인 어셈블리 코드 해설은 다음과 같다.
스위치 문 구현 (어셈블리 해설)
-
범위 검사용 직접 비교
3418089c: cmp r0, #53 341808a0: bgt.n 0x341808e0 ; choice > 53 이면 Default(0x341808e0)로choice가 53보다 크면 0x341808e0(기본(default) 처리)로 분기.
-
특정 값(147) 직접 처리
0x341808e0: cmp r0, #147 bne.n 0x341808f2 ; choice != 147 이면 Default(0x341808f2) mov.w r3, #400 str r3, [sp, #20] ; temp_val_case4 = 400 ldr r0, [sp, #20] adds r0, #20 ; ret = 400 + 20 = 420 b Epilogue -
다른 낮은 범위(⇐53) 안에서 1과 22 직접 처리
341808a2: cmp r0, #22 341808a4: ble.n 0x341808d0 ; choice <= 22 이면 0x341808d0으로choice <= 22이면:0x341808d0: cmp r0, #1 bne.n 0x341808f2 ; choice != 1 이면 Default movs r3, #100 str r3, [sp, #4] ; temp_val_case1 = 100 ldr r0, [sp, #4] adds r0, #5 ; ret = 100 + 5 = 105 b Epilogue
-
밀집된 값(23~53)용 점프 테이블 준비
341808a6: subs r0, #23 ; r0 = choice – 23 → 0부터 인덱스 341808a8: cmp r0, #30 ; 최대 인덱스 30(53–23) 341808aa: bhi.n 0x341808f2 ; 인덱스 > 30 이면 Default -
점프 테이블 실행 (
tbb)341808ac: tbb [pc, r0]pc(현재 명령어 주소+4) 기준으로,r0에 담긴 인덱스만큼 떨어진 테이블 바이트를 읽어- 읽은 값 × 2만큼 더한 위치로 분기한다.
-
테이블 데이터 예시 및 분기 대상
- 테이블 바이트(인덱스 0…30)는 각
case레이블의 오프셋을 2로 나눈 값이다. - 계산식:
choice=23(인덱스0) → 바이트 0x27 → 분기: PC_base + 0x27×2 = 0x341808b0 + 0x4E = 0x341808FEchoice=37(인덱스14) → 바이트 0x2D → 분기: 0x341808b0 + 0x5A = 0x3418090Achoice=47(인덱스24) → 바이트 0x34 → 분기: 0x341808b0 + 0x68 = 0x34180918choice=53(인덱스30) → 바이트 0x3B → 분기: 0x341808b0 + 0x76 = 0x34180926- 그 외(인덱스 값이 1~29) → 바이트 0x21 → 분기: 0x341808b0 + 0x42 = 0x341808F2 → Default
- 테이블 바이트(인덱스 0…30)는 각
-
케이스 핸들러 (테이블 분기 또는 직접 분기)
-
Default (0x341808f2)
mov.w r3, #0xFFFFFFFF ; -1 str r3, [sp, #28] ; temp_val_default = -1 ldr r0, [sp, #28] b Epilogue ; ret = -1 -
choice=23 핸들러(0x341808fe)
movs r3, #200 str r3, [sp, #8] ldr r0, [sp, #8] adds r0, #10 ; ret = 210 b Epilogue -
choice=37 핸들러(0x3418090a)
mov.w r3, #400 str r3, [sp, #24] ldr r0, [sp, #24] adds r0, #20 ; ret = 420 b Epilogue -
choice=47 핸들러(0x34180918)
mov.w r3, #400 str r3, [sp, #16] ldr r0, [sp, #16] adds r0, #20 ; ret = 420 b Epilogue -
choice=53 핸들러(0x34180926)
mov.w r3, #300 str r3, [sp, #12] ldr r0, [sp, #12] adds r0, #15 ; ret = 315 b Epilogue
-
-
에필로그 (공통)
Epilogue: add sp, #32 ; 스택 해제 bx lr ; 호출자 복귀 (r0에 리턴값)
동작 요약
choice=1→105반환choice=23→210반환choice=37→420반환choice=47→420반환choice=53→315반환choice=147→420반환- 그 외 모든 값 →
1반환
이후부터는 글의 분량상 어셈블리 코드 해설을 제외하겠다.
연속적인 case label이 나누어진다면?
#include <stdint.h>
__attribute__((noinline)) // Prevent inlining
int test(int choice) {
int ret;
switch (choice) {
case 10: {
volatile int temp_val_case1 = 100;
ret = temp_val_case1 + 5;
break;
}
case 11: {
volatile int temp_val_case2 = 200;
ret = temp_val_case2 + 10;
break;
}
case 12: {
volatile int temp_val_case3 = 300;
ret = temp_val_case3 + 15;
break;
}
case 13: {
volatile int temp_val_case3 = 300;
ret = temp_val_case3 + 15;
break;
}
case 100: {
volatile int temp_val_case4 = 400;
ret = temp_val_case4 + 20;
break;
}
case 101: {
volatile int temp_val_case4 = 400;
ret = temp_val_case4 + 20;
break;
}
case 102: {
volatile int temp_val_case4 = 400;
ret = temp_val_case4 + 20;
break;
}
case 131: {
volatile int temp_val_case4 = 400;
ret = temp_val_case4 + 20;
break;
}
default: {
volatile int temp_val_default = -1;
ret = temp_val_default;
break;
}
}
return ret;
} test:
3418089c: cmp r0, #13
220 int test(int choice) {
3418089e: sub sp, #40 @ 0x28
222 switch (choice) {
341808a0: ble.n 0x341808d0 <test+52>
341808a2: subs r0, #100 @ 0x64
341808a4: cmp r0, #31
341808a6: bhi.n 0x341808e2 <test+70>
341808a8: cmp r0, #31
341808aa: bhi.n 0x341808e2 <test+70>
341808ac: tbb [pc, r0]
341808b0: adcs r0, r1
341808b2: adds r2, r7, r4
341808b4: adds r1, r3, r4
341808b6: adds r1, r3, r4
341808b8: adds r1, r3, r4
341808ba: adds r1, r3, r4
341808bc: adds r1, r3, r4
341808be: adds r1, r3, r4
341808c0: adds r1, r3, r4
341808c2: adds r1, r3, r4
341808c4: adds r1, r3, r4
341808c6: adds r1, r3, r4
341808c8: adds r1, r3, r4
341808ca: adds r1, r3, r4
341808cc: adds r1, r3, r4
341808ce: adds r3, #25
341808d0: cmp r0, #9
341808d2: ble.n 0x341808e2 <test+70>
341808d4: subs r0, #11
341808d6: cmp r0, #2
341808d8: bhi.n 0x3418094e <test+178>
341808da: tbb [pc, r0]
341808de: lsrs r7, r1, #32
341808e0: movs r5, r2
264 volatile int temp_val_default = -1;
341808e2: mov.w r3, #4294967295
341808e6: str r3, [sp, #36] @ 0x24
265 ret = temp_val_default;
341808e8: ldr r0, [sp, #36] @ 0x24
270 }
341808ea: add sp, #40 @ 0x28
341808ec: bx lr
234 volatile int temp_val_case3 = 300;
341808ee: mov.w r3, #300 @ 0x12c
341808f2: str r3, [sp, #12]
235 ret = temp_val_case3 + 15;
341808f4: ldr r0, [sp, #12]
341808f6: adds r0, #15
341808f8: add sp, #40 @ 0x28
341808fa: bx lr
229 volatile int temp_val_case2 = 200;
341808fc: movs r3, #200 @ 0xc8
341808fe: str r3, [sp, #8]
230 ret = temp_val_case2 + 10;
34180900: ldr r0, [sp, #8]
34180902: adds r0, #10
270 }
34180904: add sp, #40 @ 0x28
34180906: bx lr
239 volatile int temp_val_case3 = 300;
34180908: mov.w r3, #300 @ 0x12c
3418090c: str r3, [sp, #16]
240 ret = temp_val_case3 + 15;
3418090e: ldr r0, [sp, #16]
34180910: adds r0, #15
270 }
34180912: add sp, #40 @ 0x28
34180914: bx lr
259 volatile int temp_val_case4 = 400;
34180916: mov.w r3, #400 @ 0x190
3418091a: str r3, [sp, #32]
260 ret = temp_val_case4 + 20;
3418091c: ldr r0, [sp, #32]
3418091e: adds r0, #20
270 }
34180920: add sp, #40 @ 0x28
34180922: bx lr
254 volatile int temp_val_case4 = 400;
34180924: mov.w r3, #400 @ 0x190
34180928: str r3, [sp, #28]
255 ret = temp_val_case4 + 20;
3418092a: ldr r0, [sp, #28]
3418092c: adds r0, #20
270 }
3418092e: add sp, #40 @ 0x28
34180930: bx lr
249 volatile int temp_val_case4 = 400;
34180932: mov.w r3, #400 @ 0x190
34180936: str r3, [sp, #24]
250 ret = temp_val_case4 + 20;
34180938: ldr r0, [sp, #24]
3418093a: adds r0, #20
270 }
3418093c: add sp, #40 @ 0x28
3418093e: bx lr
244 volatile int temp_val_case4 = 400;
34180940: mov.w r3, #400 @ 0x190
34180944: str r3, [sp, #20]
245 ret = temp_val_case4 + 20;
34180946: ldr r0, [sp, #20]
34180948: adds r0, #20
270 }
3418094a: add sp, #40 @ 0x28
3418094c: bx lr
224 volatile int temp_val_case1 = 100;
3418094e: movs r3, #100 @ 0x64
34180950: str r3, [sp, #4]
225 ret = temp_val_case1 + 5;
34180952: ldr r0, [sp, #4]
34180954: adds r0, #5
226 break;
34180956: b.n 0x341808ea <test+78>
278 volatile int a = 2;위 코드를 살펴보인 4개 이상의 연속성이 발견되면 ttb명령을 통해서 테이블을 추가로 생성하는 것을 볼 수 있다.
다음은 주요 분기 코드에 관한 해설이다.
3418089c: cmp r0, #13
...
341808a0: ble.n 0x341808d0 <test+52> // r0 <= 13 라면 관련 table처리로 점프
341808a2: subs r0, #100 // 100이상 값들이므로 -100 (인덱스 0~31로 조정)
341808a4: cmp r0, #31
341808a6: bhi.n 0x341808e2 <test+70> // r0 > 31 라면 default 처리
// 아니라면 100 ~ 131 인덱스 처리이처럼 연속성이 3개 이하지만, 32개의 범위내에 인덱스가 존재한다면 테이블로 생성한다.
32개를 를 넘어설 경우 if-else 체이닝으로 처리된다.
연속적인 경우의 테이블 인덱스 생성
__attribute__((noinline)) // Prevent inlining
int test(int choice) {
int ret;
switch (choice) {
case 0: { volatile int v = 0; ret = v + 1; break; }
case 1: { volatile int v = 1; ret = v + 1; break; }
case 2: { volatile int v = 2; ret = v + 1; break; }
case 3: { volatile int v = 3; ret = v + 1; break; }
case 4: { volatile int v = 4; ret = v + 1; break; }
case 5: { volatile int v = 5; ret = v + 1; break; }
case 6: { volatile int v = 6; ret = v + 1; break; }
case 7: { volatile int v = 7; ret = v + 1; break; }
case 8: { volatile int v = 8; ret = v + 1; break; }
case 9: { volatile int v = 9; ret = v + 1; break; }
case 10: { volatile int v = 100; ret = v + 5; break; } // customized
case 11: { volatile int v = 200; ret = v + 10; break; } // customized
case 12: { volatile int v = 300; ret = v + 15; break; } // customized
case 13: { volatile int v = 300; ret = v + 15; break; } // customized
case 14: { volatile int v = 14; ret = v + 1; break; }
case 15: { volatile int v = 15; ret = v + 1; break; }
case 16: { volatile int v = 16; ret = v + 1; break; }
case 17: { volatile int v = 17; ret = v + 1; break; }
case 18: { volatile int v = 18; ret = v + 1; break; }
case 19: { volatile int v = 19; ret = v + 1; break; }
// ...
// (중략: 동일 패턴 반복, 예시 생략)
// ...
case 99: { volatile int v = 99; ret = v + 1; break; }
case 100:{ volatile int v = 400; ret = v + 20; break; } // customized
default: { volatile int v = -1; ret = v; break; }
}
return ret;
}어셈블리 코드 생략
위 코드와 같이 연속적인 case label을 가질 경우, case 개수와 무관하게 모두 하나의 테이블로 분기를 처리한다.
적어도 808개 까지는 그렇게 동작하는듯 하다.
하지만 연속적인 중간에 이상한 수가 껴있다면 어떻게 될까?
__attribute__((noinline)) // Prevent inlining
int test(int choice) {
int ret;
switch (choice) {
case 0: { volatile int v = 0; ret = v + 1; break; }
case 1: { volatile int v = 1; ret = v + 1; break; }
case 2: { volatile int v = 2; ret = v + 1; break; }
case 3: { volatile int v = 3; ret = v + 1; break; }
case 4: { volatile int v = 4; ret = v + 1; break; }
case 5: { volatile int v = 5; ret = v + 1; break; }
case 6: { volatile int v = 6; ret = v + 1; break; }
case 7: { volatile int v = 7; ret = v + 1; break; }
case 8: { volatile int v = 8; ret = v + 1; break; }
case 9: { volatile int v = 9; ret = v + 1; break; }
case 10: { volatile int v = 100; ret = v + 5; break; }
case 11: { volatile int v = 200; ret = v + 10; break; }
case 12: { volatile int v = 300; ret = v + 15; break; }
case 13: { volatile int v = 300; ret = v + 15; break; }
case 14: { volatile int v = 14; ret = v + 1; break; }
case 15: { volatile int v = 15; ret = v + 1; break; }
case 16: { volatile int v = 16; ret = v + 1; break; }
case 17: { volatile int v = 17; ret = v + 1; break; }
case 18: { volatile int v = 18; ret = v + 1; break; }
case 19: { volatile int v = 19; ret = v + 1; break; }
// ...
// (중략: 동일 패턴 반복, 예시 생략)
// ...
case 807: { volatile int v = 99; ret = v + 1; break; } // 인덱스 807
case 100:{ volatile int v = 400; ret = v + 20; break; }
default: { volatile int v = -1; ret = v; break; }
}
return ret;
}결론부터 보자면, 연속된 숫자 뒤에 다른 수가 하나 껴있으면 808개 까지 인덱스를 생성한다…
즉, 인덱스가 2바이트로 저장되므로. 최악의 경우 1616바이트(1.6K) 만큼 바이너리 코드를 증가시킬 수 있다.
하지만 이를 -Osize옵션으로 처리할 경우에는 if-else로 분기를 나누고 나머지만 인덱스로 처리하는 것을 볼 수 있다. 이처럼 최적화 옵션에 따라서 다르게 처리된다. 해당 결과를 통해서 속도에 과도하게 치중할 경우, 비효율이 생길 수 있다는 교훈을 얻었다.
결론
- 조금 긴
case label을 가지는switch-case문법의 경우에는 최대한 연속적으로 배치하는 것이 좋다. - 비연속적인
switch-case는 컴파일러 & 최적화 옵션에 따라서 일정하지 않게 구현된다. 따라서 코드를disassemble하여 직접 확인하는것이 정확하다.
나의 테스트 환경에서는, 글에서 설명한 switch-case의 최적화 방식과 if-else가 동일하게 최적화됐다.
테스트 환경
OS: Window11
IDE & ToolChain : STM32Cube
Board: STM32N6-DK
option: -Ofast