Search
Duplicate
🥕

if-else문과 switch문의 차이 (LUT)

간단소개
switch문 컴파일러 최적화
팔만코딩경 컨트리뷰터
ContributorNotionAccount
주제 / 분류
C
C++
Assembly
CS
Scrap
태그
컴파일러
9 more properties

if-else와 switch는 뭐가 다를까?

사실 최근 컴파일러에 와서는 차이가 없다. 설명은 20년 전 기준이라고 생각하자. 중요한 건 내부적으로 어떻게 컴파일러가 최적화 해주냐는 것이다
일단 들어온 값이 특정한 값을 리턴하는 함수를 작성해 보자. 아래와 같이 switch문을 작성 해보자.
int test(int n) { switch (n) { case 0: return 5; case 1: // fallthrough case 2: case 3: return 10; case 5: // fallthrough case 7: return 15; default: return 0; } }
C
복사
이 코드를 if문 처럼 케이스마다 하나하나 비교한다고 생각해보면 너무 비효율적이다. 그렇게 분기가 많이 생기면, CPU 파이프라인에서 그만큼 Stall이 생긴다. 분기예측으로 Stall을 상쇄할 순 있으나, 이런 류의 코드는 분기예측 확률이 높지 않을 가능성이 높을것이다.
굳이 비교할 필요가 없이 테이블을 만들어 한번에 해결할 수 있다. C언어로 예시를 보자.
int test(int n) { const int LUT[8] = { 5, 10, 10, 10, 0, 15, 0, 15 }; if ((unsigned int)n > 7u) { return 0; } return LUT[n]; }
C
복사
배열 테이블을 만들어 미리 return값을 다 넣어두고, n이 0-7 범위 밖인 경우를 처리한 뒤, 추가적인 분기 없이 한번에 return 해버린다. (이걸 주로 LUT(LookUp-Table)라고 부른다)
이렇게 되면 분기가 한 번밖에 생기지 않는다. 그런데 실제로 컴파일러도 이렇게 최적화를 해준다
;Clang x86-64 test(int): # @test(int) xor eax, eax cmp edi, 7 ja .LBB0_2 movsxd rax, edi mov eax, dword ptr [4*rax + .Lswitch.table.test(int)] .LBB0_2: ret .Lswitch.table.test(int): .long 5 # 0x5 .long 10 # 0xa .long 10 # 0xa .long 10 # 0xa .long 0 # 0x0 .long 15 # 0xf .long 0 # 0x0 .long 15 # 0xf
C
복사
보면 아까 c에서 LUT를 만든 것과 같이, 컴파일러도 테이블을 만든다. 테이블 범위 이외는 default로 0 반환. 테이블 범위 이내는 바로 테이블 값으로 바로 반환.
n이 0~7만 무조건 들어온다고 가정한다면 맨 앞의 cmp를 없애 더욱 최적화 시킬 수 있겠다. C언어 코드를 바꿔보자.
#include <assert.h> #define NDEBUG int test(int n) { switch (n) { case 0: return 5; case 1: // fallthrough case 2: case 3: return 10; case 4: // fallthrough case 6: return 0; case 5: // fallthrough case 7: return 15; default: __builtin_unreachable(); //GCC hint // or __assume(0); //MSVC hint // or assert(0); } }
C
복사
이러면 컴파일러는 0-7 이외 다른 값이 들어오지 않는다 가정하고 release 빌드에서 범위를 체크하는 cmp를 없앤다.
test(int): # @test(int) movsxd rax, edi lea rcx, [rip + .Lswitch.table.test(int)] mov eax, dword ptr [rcx + 4*rax] ret .Lswitch.table.test(int): .long 5 # 0x5 .long 10 # 0xa .long 10 # 0xa .long 10 # 0xa .long 0 # 0x0 .long 15 # 0xf .long 0 # 0x0 .long 15 # 0xf
C
복사
이렇게 분기를 아예 없애버려서 성능을 많이 올릴 수 있다.
사실 요즘엔 if-else 도 컴파일러가 알아서 결정해서 이 방식으로 최적화 해버린다. 그래도 내부 최적화가 어떻게 진행되는지 안다면, 위처럼 default 케이스를 제거하는 식으로 컴파일러에게 적극적인 성능향상 힌트를 제공할 수 있다.

+Jump table

물론 LUT를 값으로만 사용하는것이 아닌 주소로 사용하여 점프테이블로 사용할 수도 있다.
대충 이런식.
lea rbx, <jmp_table> mov eax, [rbx + rcx * 8] jmp [eax] .jmp_table .DQ 0x80000000 # <switch_case0> .DQ 0x12345678 # <switch_case1> .DQ 0x98763263 # <switch_case2>
Assembly
복사