'스위치'가 'if'보다 빠릅니까?


242

A는 switch문은 실제로 빠를 것보다 if문?

/Ox플래그를 사용하여 Visual Studio 2010의 x64 C ++ 컴파일러에서 아래 코드를 실행했습니다 .

#include <stdlib.h>
#include <stdio.h>
#include <time.h>

#define MAX_COUNT (1 << 29)
size_t counter = 0;

size_t testSwitch()
{
    clock_t start = clock();
    size_t i;
    for (i = 0; i < MAX_COUNT; i++)
    {
        switch (counter % 4 + 1)
        {
            case 1: counter += 4; break;
            case 2: counter += 3; break;
            case 3: counter += 2; break;
            case 4: counter += 1; break;
        }
    }
    return 1000 * (clock() - start) / CLOCKS_PER_SEC;
}

size_t testIf()
{
    clock_t start = clock();
    size_t i;
    for (i = 0; i < MAX_COUNT; i++)
    {
        const size_t c = counter % 4 + 1;
        if (c == 1) { counter += 4; }
        else if (c == 2) { counter += 3; }
        else if (c == 3) { counter += 2; }
        else if (c == 4) { counter += 1; }
    }
    return 1000 * (clock() - start) / CLOCKS_PER_SEC;
}

int main()
{
    printf("Starting...\n");
    printf("Switch statement: %u ms\n", testSwitch());
    printf("If     statement: %u ms\n", testIf());
}

그리고이 결과를 얻었다 :

스위치 문 : 5261ms
If 문 : 5196ms

내가 배운 내용에서 switch명령문은 분명히 분기 테이블을 최적화하기 위해 점프 테이블을 사용합니다.

질문 :

  1. x86 또는 x64에서 기본 점프 테이블은 어떤 모양입니까?

  2. 이 코드는 점프 테이블을 사용합니까?

  3. 이 예제에서 성능 차이가없는 이유는 무엇입니까? 거기에있는 어떤 상황 거기에 있다 상당한 성능 차이는?


코드 분해 :

testIf:

13FE81B10 sub  rsp,48h 
13FE81B14 call qword ptr [__imp_clock (13FE81128h)] 
13FE81B1A mov  dword ptr [start],eax 
13FE81B1E mov  qword ptr [i],0 
13FE81B27 jmp  testIf+26h (13FE81B36h) 
13FE81B29 mov  rax,qword ptr [i] 
13FE81B2E inc  rax  
13FE81B31 mov  qword ptr [i],rax 
13FE81B36 cmp  qword ptr [i],20000000h 
13FE81B3F jae  testIf+0C3h (13FE81BD3h) 
13FE81B45 xor  edx,edx 
13FE81B47 mov  rax,qword ptr [counter (13FE835D0h)] 
13FE81B4E mov  ecx,4 
13FE81B53 div  rax,rcx 
13FE81B56 mov  rax,rdx 
13FE81B59 inc  rax  
13FE81B5C mov  qword ptr [c],rax 
13FE81B61 cmp  qword ptr [c],1 
13FE81B67 jne  testIf+6Dh (13FE81B7Dh) 
13FE81B69 mov  rax,qword ptr [counter (13FE835D0h)] 
13FE81B70 add  rax,4 
13FE81B74 mov  qword ptr [counter (13FE835D0h)],rax 
13FE81B7B jmp  testIf+0BEh (13FE81BCEh) 
13FE81B7D cmp  qword ptr [c],2 
13FE81B83 jne  testIf+89h (13FE81B99h) 
13FE81B85 mov  rax,qword ptr [counter (13FE835D0h)] 
13FE81B8C add  rax,3 
13FE81B90 mov  qword ptr [counter (13FE835D0h)],rax 
13FE81B97 jmp  testIf+0BEh (13FE81BCEh) 
13FE81B99 cmp  qword ptr [c],3 
13FE81B9F jne  testIf+0A5h (13FE81BB5h) 
13FE81BA1 mov  rax,qword ptr [counter (13FE835D0h)] 
13FE81BA8 add  rax,2 
13FE81BAC mov  qword ptr [counter (13FE835D0h)],rax 
13FE81BB3 jmp  testIf+0BEh (13FE81BCEh) 
13FE81BB5 cmp  qword ptr [c],4 
13FE81BBB jne  testIf+0BEh (13FE81BCEh) 
13FE81BBD mov  rax,qword ptr [counter (13FE835D0h)] 
13FE81BC4 inc  rax  
13FE81BC7 mov  qword ptr [counter (13FE835D0h)],rax 
13FE81BCE jmp  testIf+19h (13FE81B29h) 
13FE81BD3 call qword ptr [__imp_clock (13FE81128h)] 
13FE81BD9 sub  eax,dword ptr [start] 
13FE81BDD imul eax,eax,3E8h 
13FE81BE3 cdq       
13FE81BE4 mov  ecx,3E8h 
13FE81BE9 idiv eax,ecx 
13FE81BEB cdqe      
13FE81BED add  rsp,48h 
13FE81BF1 ret       

testSwitch:

13FE81C00 sub  rsp,48h 
13FE81C04 call qword ptr [__imp_clock (13FE81128h)] 
13FE81C0A mov  dword ptr [start],eax 
13FE81C0E mov  qword ptr [i],0 
13FE81C17 jmp  testSwitch+26h (13FE81C26h) 
13FE81C19 mov  rax,qword ptr [i] 
13FE81C1E inc  rax  
13FE81C21 mov  qword ptr [i],rax 
13FE81C26 cmp  qword ptr [i],20000000h 
13FE81C2F jae  testSwitch+0C5h (13FE81CC5h) 
13FE81C35 xor  edx,edx 
13FE81C37 mov  rax,qword ptr [counter (13FE835D0h)] 
13FE81C3E mov  ecx,4 
13FE81C43 div  rax,rcx 
13FE81C46 mov  rax,rdx 
13FE81C49 inc  rax  
13FE81C4C mov  qword ptr [rsp+30h],rax 
13FE81C51 cmp  qword ptr [rsp+30h],1 
13FE81C57 je   testSwitch+73h (13FE81C73h) 
13FE81C59 cmp  qword ptr [rsp+30h],2 
13FE81C5F je   testSwitch+87h (13FE81C87h) 
13FE81C61 cmp  qword ptr [rsp+30h],3 
13FE81C67 je   testSwitch+9Bh (13FE81C9Bh) 
13FE81C69 cmp  qword ptr [rsp+30h],4 
13FE81C6F je   testSwitch+0AFh (13FE81CAFh) 
13FE81C71 jmp  testSwitch+0C0h (13FE81CC0h) 
13FE81C73 mov  rax,qword ptr [counter (13FE835D0h)] 
13FE81C7A add  rax,4 
13FE81C7E mov  qword ptr [counter (13FE835D0h)],rax 
13FE81C85 jmp  testSwitch+0C0h (13FE81CC0h) 
13FE81C87 mov  rax,qword ptr [counter (13FE835D0h)] 
13FE81C8E add  rax,3 
13FE81C92 mov  qword ptr [counter (13FE835D0h)],rax 
13FE81C99 jmp  testSwitch+0C0h (13FE81CC0h) 
13FE81C9B mov  rax,qword ptr [counter (13FE835D0h)] 
13FE81CA2 add  rax,2 
13FE81CA6 mov  qword ptr [counter (13FE835D0h)],rax 
13FE81CAD jmp  testSwitch+0C0h (13FE81CC0h) 
13FE81CAF mov  rax,qword ptr [counter (13FE835D0h)] 
13FE81CB6 inc  rax  
13FE81CB9 mov  qword ptr [counter (13FE835D0h)],rax 
13FE81CC0 jmp  testSwitch+19h (13FE81C19h) 
13FE81CC5 call qword ptr [__imp_clock (13FE81128h)] 
13FE81CCB sub  eax,dword ptr [start] 
13FE81CCF imul eax,eax,3E8h 
13FE81CD5 cdq       
13FE81CD6 mov  ecx,3E8h 
13FE81CDB idiv eax,ecx 
13FE81CDD cdqe      
13FE81CDF add  rsp,48h 
13FE81CE3 ret       

최신 정보:

여기에 흥미로운 결과가 있습니다 . 그러나 왜 하나가 더 빠르며 느린 지 잘 모르겠습니다.


47
사람들은 지구상에서이 생각을 끝내기 위해 무엇을 투표하고 있습니까? 그들은 이상적인 코드보다 적게 생성하는 것에 대한 생각이 이단이라는 완벽하게 최적화 된 컴파일러의 개념을 믿는가? 최적화 에 대한 아이디어는 어디에서나 그들을 화나게합니까?
Crashworks

6
이 질문에 정확히 무엇이 잘못 되었습니까?
Tugrul Ates

25
이 질문에 어떤 문제가 있는지 궁금해 하는 사람에게는 : 처음 에는 질문 이 아니라 3 가지 질문이므로 많은 답변이 서로 다른 문제를 해결합니다. 이것은 모든 것에 대한 답을 받아들이 기가 어려울 것이라는 것을 의미한다 . 또한, 위의 질문에 대한 전형적인 무질서한 반응은 대부분이 수준의 최적화에서 거의 항상 조기에 최적화 한다는 사실로 인해 "실제로 흥미롭지는 않습니다"라고 닫는 것 입니다. 마지막으로, 5196 대 5261은 실제로 관리하기에 충분하지 않아야합니다. 이해하기 쉬운 논리 코드를 작성하십시오.
Lasse V. Karlsen

40
@Lasse : SO 대신 세 가지 질문 을 게시하는 것을 정말로 원하십니까 ? 또한 : -> 당신이 질문을 오해했는지 또는 내가 당신의 의견을 오해하는지 확실하지 않지만, 왜 차이 없는지 묻는 나의 질문의 요점이 아닌가? (내가 신경 써야 할 중요한 차이점이라고 주장한 적이 있는가?)5196 vs. 5261 shouldn't be enough to actually care
user541686

5
@Robert : 글쎄요. 메타 주석이기 때문에 20 개 이상의 주석 만 있습니다. 실제로이 질문과 관련된 의견은 7 개뿐입니다. 의견 : 여기에 "의견"이 어떻게 보이는지 모르겠습니다. 성능 차이가 보이지 않는 이유 가 있습니다 . 그냥 맛 있니? 토론 : 어쩌면 다른 곳에서 본 것처럼 건강한 토론처럼 보입니다. (반대하는 것이 있는지 알려주세요). 논증 : 여기서는 논쟁의 여지가 없습니다 ( '논쟁'의 동의어로 간주하지 않는 한)? 확장 된 토론 : 이러한 메타 주석을 포함하는 경우.
user541686

답변:


122

컴파일러 가 스위치에서 수행 할 수 있는 몇 가지 최적화가 있습니다 . 나는 종종 언급 된 "점프 테이블"은 입력이 어떤 식 으로든 바인딩 될 수있을 때만 작동하기 때문에 매우 유용한 것이라고 생각하지 않습니다.

C "점프 테이블"에 대한 의사 코드는 다음과 같습니다 . 실제로 컴파일러는 테이블에서 입력이 유효한지 확인하기 위해 테이블 ​​주위에 if 테스트 형식을 삽입해야합니다. 또한 입력이 연속적인 숫자의 연속 인 특정 경우에만 작동합니다.

스위치의 분기 수가 매우 많은 경우 컴파일러는 스위치 값에 대해 이진 검색을 사용하는 것과 같은 작업을 수행 할 수 있습니다. 시나리오는 스위치만큼 일반적이며 코드 크기가 더 커지지 않습니다. 그러나 그것을 확인하려면 테스트 코드에 차이가 있는지 더 많은 분기가 필요합니다.

특정 질문에 대답하려면 다음을 수행하십시오.

  1. Clang은 다음과 같은 것을 생성 합니다 :

    test_switch(char):                       # @test_switch(char)
            movl    %edi, %eax
            cmpl    $19, %edi
            jbe     .LBB0_1
            retq
    .LBB0_1:
            jmpq    *.LJTI0_0(,%rax,8)
            jmp     void call<0u>()         # TAILCALL
            jmp     void call<1u>()         # TAILCALL
            jmp     void call<2u>()         # TAILCALL
            jmp     void call<3u>()         # TAILCALL
            jmp     void call<4u>()         # TAILCALL
            jmp     void call<5u>()         # TAILCALL
            jmp     void call<6u>()         # TAILCALL
            jmp     void call<7u>()         # TAILCALL
            jmp     void call<8u>()         # TAILCALL
            jmp     void call<9u>()         # TAILCALL
            jmp     void call<10u>()        # TAILCALL
            jmp     void call<11u>()        # TAILCALL
            jmp     void call<12u>()        # TAILCALL
            jmp     void call<13u>()        # TAILCALL
            jmp     void call<14u>()        # TAILCALL
            jmp     void call<15u>()        # TAILCALL
            jmp     void call<16u>()        # TAILCALL
            jmp     void call<17u>()        # TAILCALL
            jmp     void call<18u>()        # TAILCALL
            jmp     void call<19u>()        # TAILCALL
    .LJTI0_0:
            .quad   .LBB0_2
            .quad   .LBB0_3
            .quad   .LBB0_4
            .quad   .LBB0_5
            .quad   .LBB0_6
            .quad   .LBB0_7
            .quad   .LBB0_8
            .quad   .LBB0_9
            .quad   .LBB0_10
            .quad   .LBB0_11
            .quad   .LBB0_12
            .quad   .LBB0_13
            .quad   .LBB0_14
            .quad   .LBB0_15
            .quad   .LBB0_16
            .quad   .LBB0_17
            .quad   .LBB0_18
            .quad   .LBB0_19
            .quad   .LBB0_20
            .quad   .LBB0_21
  2. 점프 테이블을 사용하지 않는다고 말할 수 있습니다 .4 개의 비교 명령이 명확하게 보입니다.

    13FE81C51 cmp  qword ptr [rsp+30h],1 
    13FE81C57 je   testSwitch+73h (13FE81C73h) 
    13FE81C59 cmp  qword ptr [rsp+30h],2 
    13FE81C5F je   testSwitch+87h (13FE81C87h) 
    13FE81C61 cmp  qword ptr [rsp+30h],3 
    13FE81C67 je   testSwitch+9Bh (13FE81C9Bh) 
    13FE81C69 cmp  qword ptr [rsp+30h],4 
    13FE81C6F je   testSwitch+0AFh (13FE81CAFh) 

    점프 테이블 기반 솔루션은 비교를 전혀 사용하지 않습니다.

  3. 컴파일러가 점프 테이블을 생성 할 수있는 분기가 충분하지 않거나 컴파일러가 단순히 분기 테이블을 생성하지 않습니다. 확실하지 않습니다.

EDIT 2014 : LLVM 옵티 마이저에 익숙한 사람들과 다른 곳에서 점프 테이블 최적화가 많은 시나리오에서 중요 할 수 있다고 이야기했습니다. 예를 들어 많은 값을 가진 열거가 있고 상기 열거 된 값에 대해 많은 경우가 있습니다. 즉, 나는 2011 년에 내가 위에서 말한 것을지지합니다. 너무 자주 사람들이 "내가 스위치를 만들면 얼마나 많은 경우에 상관없이 같은 시간이 될 것"이라고 생각하는 것을 보게됩니다. 점프 테이블을 사용하더라도 간접 점프 비용이 발생하며 각 경우에 대해 테이블의 항목에 대해 비용을 지불합니다. 메모리 대역폭은 최신 하드웨어에서 큰 문제입니다.

가독성을위한 코드를 작성하십시오. 그 가치가있는 컴파일러는 if / else if 사다리를보고 더 빠른 경우 동등한 스위치로 또는 그 반대로 변환합니다.


3
실제로 질문에 답변하고 유용한 정보를 얻으려면 +1하십시오. :-) 그러나 질문 : 내가 이해 한 바에 따르면, 점프 테이블은 간접 점프를 사용 합니다. 그 맞습니까? 그렇다면 프리 페치 / 파이프 라인이 더 어려워서 일반적으로 느리지 않습니까?
user541686

1
@Mehrdad : 예, 간접 점프를 사용합니다. 그러나 하나의 간접 점프 (파이프 라인 정지와 함께)는 수백 직접 점프보다 적을 수 있습니다. :)
Billy ONeal 5

1
@Mehrdad : 아뇨, 불행히도. :( 나는 항상 IF가 더 읽기 쉽다고 생각하는 사람들의 수용소에있어서 기쁘다! :)
Billy ONeal

1
몇 가지 quips- "[스위치]는 어떤 식 으로든 입력을 묶을 수있는 경우에만 작동합니다." "테이블에서 입력이 유효한지 확인하기 위해 테이블 ​​주위에 if 형식의 테스트를 삽입해야합니다. "입력이 연속적인 숫자의 실행 인 경우"입니다. ": 잠재적으로 포인터가 읽히고 널이 아닌 값이 점프 인 경우에만 기본 테이블이있을 수 있습니다. 그렇지 않은 경우 기본 경우 그런 다음 switch출구. Soren 's는이 답변을 읽은 후 내가 말하고 싶은 몇 가지 다른 말을했습니다.
Tony Delroy

2
"그것의 가치가있는 컴파일러라면 if / else if 래더를보고 동등한 스위치로 또는 그 반대로 변환 할 것입니다."-이 주장에 대한 지원? 컴파일러는 if조항 의 순서가 빈도 및 상대적인 성능 요구에 맞게 이미 수동 조정 되었다고 가정 할 수 있습니다. 여기서는 switch전통적으로 컴파일러가 선택하는 최적화에 대한 공개 초대로 간주됩니다. 좋은 점은 과거를 뛰어 넘었다 switch:-). 코드 크기는 사례 / 범위에 따라 다릅니다. 더 좋을 수 있습니다. 마지막으로, 일부 열거 형, 비트 필드 및 char시나리오는 본질적으로 유효 / 경계 및 오버 헤드가 없습니다.
Tony Delroy

47

귀하의 질문에 :

1. x86 또는 x64에서 기본 점프 테이블의 모양은 무엇입니까?

점프 테이블은 배열 구조와 같은 레이블의 포인터를 보유하는 메모리 주소입니다. 다음 예제는 점프 테이블이 배치되는 방법을 이해하는 데 도움이됩니다.

00B14538  D8 09 AB 00 D8 09 AB 00 D8 09 AB 00 D8 09 AB 00  Ø.«.Ø.«.Ø.«.Ø.«.
00B14548  D8 09 AB 00 D8 09 AB 00 D8 09 AB 00 00 00 00 00  Ø.«.Ø.«.Ø.«.....
00B14558  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
00B14568  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................

여기에 이미지 설명을 입력하십시오

여기서 00B14538 은 점프 테이블에 대한 포인터이고 D8 09 AB 00 과 같은 값 은 레이블 포인터를 나타냅니다.

2.이 코드는 점프 테이블을 사용합니까? 이 경우에는 아니요.

3.이 예제에서 성능 차이가없는 이유는 무엇입니까?

두 경우 모두 명령이 동일하고 점프 테이블이 없기 때문에 성능 차이가 없습니다.

4. 성능 차이가 큰 상황이 있습니까?

if 점검 순서가 매우 긴 경우 ,이 경우 점프 테이블을 사용하면 성능이 향상됩니다 ( 지점을 완벽하게 예측하지 않으면 분기 / jmp 명령이 비싸지 만 메모리 비용이 발생 함).

모든 비교 명령어의 코드도 크기가 있으므로 특히 32 비트 포인터 또는 오프셋의 경우 단일 점프 테이블 조회에 실행 파일의 크기가 더 크지 않을 수 있습니다.

결론 : 컴파일러는 그러한 경우를 충분히 처리하고 적절한 지침을 생성합니다. :)


(편집 : NVM은 빌리의 대답은 이미 내가 제안 된 것을 가지고 나는 이것이 좋은 suplement에 대한 추측..)이 포함 좋은 것 gcc -S출력 : 시퀀스 .long L1/ .long L2테이블 항목은 16 진 덤프보다 더 의미있는, 누군가 그에게 더 유용하다 컴파일러를 보는 방법을 배우고 싶습니다. (스위치 코드가 간접 jmp인지 jcc인지 확인하려면 스위치 코드를 살펴보십시오).
Peter Cordes

31

컴파일러는 스위치 문을 if 문과 동등한 코드로 컴파일하거나 점프 테이블을 만들 수 있습니다. 컴파일러 옵션에서 지정한 내용에 따라 실행 속도가 가장 빠르거나 가장 작은 코드를 생성하는 방법에 따라 다른 것을 선택했을 가능성이 높습니다. 따라서 최악의 경우 if 문과 동일한 속도가됩니다.

컴파일러가 최선의 선택을하고 코드를 가장 읽기 쉽게 만드는 것에 집중할 것을 신뢰합니다.

케이스 수가 매우 커지면 점프 테이블이 일련의 if보다 훨씬 빠릅니다. 그러나 값 사이의 단계가 매우 크면 점프 테이블이 커질 수 있으며 컴파일러는 하나를 생성하지 않도록 선택할 수 있습니다.


13
나는 이것이 OP의 질문에 대답하지 않는다고 생각합니다. 조금도.
Billy ONeal

5
@Soren : 만약 이것이 "기본 질문"이라면, 그 질문에서 179 개의 다른 줄을 신경 쓰지 않았을 것입니다. 그것은 단지 한 줄일 것입니다. :-)
user541686

8
@Soren : 나는 적어도 3 참조 번호가 영업의 질문의 일환으로 하위 질문. 당신은 단지 모든 "성능"질문에 적용되는 정확히 동일한 답, 즉 먼저 측정해야하는 답보다 우선합니다. Mehrdad가 이미 측정하고이 코드를 핫스팟으로 분리했을 수 있습니다. 그러한 경우, 당신의 대답은 무가치 한 것보다 나쁘고, 그것은 소음입니다.
Billy ONeal

2
점프 테이블과 정의에 의존하지 않는 것 사이에 흐린 선이 있습니다. 하위 질문 3 부에 대한 정보를 제공했습니다.
Soren

2
@ wnoise : 그것이 유일한 정답이라면 성능 질문을 할 이유가 결코 없을 것입니다. 그러나 실제로는 소프트웨어를 측정하는 사람이 일부 있으며, 일단 측정 된 코드를 더 빨리 만드는 방법을 모르는 경우가 있습니다. Mehrdad가 질문하기 전에이 질문에 약간의 노력을 기울 였다는 것은 명백합니다. 그의 구체적인 질문은 대답하기 이상이라고 생각합니다.
Billy ONeal

13

컴퓨터가 스위치 테스트 루프 동안 테스트와 관련이없는 일부 작업을 수행하지 않았고 if 테스트 루프 동안 더 적은 작업을 수행하지 않았다는 것을 어떻게 알 수 있습니까? 테스트 결과에는 다음과 같은 내용이 표시되지 않습니다.

  1. 차이가 매우 작다
  2. 일련의 결과가 아닌 하나의 결과 만 있음
  3. 사례가 너무 적다

내 결과 :

나는 덧붙였다.

printf("counter: %u\n", counter);

카운터가 예제에서 사용되지 않았기 때문에 루프를 최적화하지 못하도록 컴파일러가 루프를 수행하는 이유는 무엇입니까? 즉시, 그러한 마이크로 벤치 마크로도 스위치는 항상 승리했습니다.

코드의 다른 문제는 다음과 같습니다.

switch (counter % 4 + 1)

스위치 루프에서

const size_t c = counter % 4 + 1; 

if 루프에서. 고치면 큰 차이가 있습니다. switch 문에 명령문을 넣으면 컴파일러가 값을 스택에 먼저 배치하지 않고 CPU 레지스터에 직접 값을 보내도록 유도합니다. 따라서 이것은 균형 잡힌 테스트가 아니라 switch 문을 선호합니다.

오, 나는 또한 테스트 사이에 카운터를 재설정해야한다고 생각합니다. 실제로, 아마도 무언가를 최적화 할 것이므로 +1, +2, +3 등 대신 임의의 종류의 난수를 사용해야합니다. 난수로, 예를 들어 현재 시간을 기준으로 한 숫자를 의미합니다. 그렇지 않으면 컴파일러는 두 함수를 하나의 긴 수학 연산으로 바꿀 수 있으며 루프를 방해하지 않을 수도 있습니다.

컴파일러가 코드를 실행하기 전에 알아낼 수 없도록 Ryan의 코드를 수정했습니다.

#include <stdlib.h>
#include <stdio.h>
#include <time.h>

#define MAX_COUNT (1 << 26)
size_t counter = 0;

long long testSwitch()
{
    clock_t start = clock();
    size_t i;
    for (i = 0; i < MAX_COUNT; i++)
    {
        const size_t c = rand() % 20 + 1;

        switch (c)
        {
                case 1: counter += 20; break;
                case 2: counter += 33; break;
                case 3: counter += 62; break;
                case 4: counter += 15; break;
                case 5: counter += 416; break;
                case 6: counter += 3545; break;
                case 7: counter += 23; break;
                case 8: counter += 81; break;
                case 9: counter += 256; break;
                case 10: counter += 15865; break;
                case 11: counter += 3234; break;
                case 12: counter += 22345; break;
                case 13: counter += 1242; break;
                case 14: counter += 12341; break;
                case 15: counter += 41; break;
                case 16: counter += 34321; break;
                case 17: counter += 232; break;
                case 18: counter += 144231; break;
                case 19: counter += 32; break;
                case 20: counter += 1231; break;
        }
    }
    return 1000 * (long long)(clock() - start) / CLOCKS_PER_SEC;
}

long long testIf()
{
    clock_t start = clock();
    size_t i;
    for (i = 0; i < MAX_COUNT; i++)
    {
        const size_t c = rand() % 20 + 1;
        if (c == 1) { counter += 20; }
        else if (c == 2) { counter += 33; }
        else if (c == 3) { counter += 62; }
        else if (c == 4) { counter += 15; }
        else if (c == 5) { counter += 416; }
        else if (c == 6) { counter += 3545; }
        else if (c == 7) { counter += 23; }
        else if (c == 8) { counter += 81; }
        else if (c == 9) { counter += 256; }
        else if (c == 10) { counter += 15865; }
        else if (c == 11) { counter += 3234; }
        else if (c == 12) { counter += 22345; }
        else if (c == 13) { counter += 1242; }
        else if (c == 14) { counter += 12341; }
        else if (c == 15) { counter += 41; }
        else if (c == 16) { counter += 34321; }
        else if (c == 17) { counter += 232; }
        else if (c == 18) { counter += 144231; }
        else if (c == 19) { counter += 32; }
        else if (c == 20) { counter += 1231; }
    }
    return 1000 * (long long)(clock() - start) / CLOCKS_PER_SEC;
}

int main()
{
    srand(time(NULL));
    printf("Starting...\n");
    printf("Switch statement: %lld ms\n", testSwitch()); fflush(stdout);
    printf("counter: %d\n", counter);
    counter = 0;
    srand(time(NULL));
    printf("If     statement: %lld ms\n", testIf()); fflush(stdout);
    printf("counter: %d\n", counter);
} 

스위치 : 3740
경우 : 3980

(여러 번의 시도와 유사한 결과)

또한 케이스 / if 수를 5로 줄 였는데도 스위치 기능은 여전히 ​​성공했습니다.


Idk, 나는 그것을 증명할 수 없다; 다른 결과를 얻습니까?
user541686

+1 : 벤치마킹이 어렵고 일반 컴퓨터에서 한 번의 실행으로 작은 시차로 결론을 도출 할 수 없습니다. 많은 수의 테스트를 실행하고 결과에 대한 일부 통계를 수행 할 수 있습니다. 또는 에뮬레이터에서 제어 된 실행시 프로세서주기를 계산합니다.
Thomas Padron-McCarthy

어, 정확히 어디 에서 print진술 을 추가 했 습니까? 전체 프로그램 끝에 추가했지만 아무런 차이가 없었습니다. 또한 다른 것과의 "문제"가 무엇인지 이해하지 못합니다. "매우 큰 차이"가 무엇인지 설명하는 것이 마음에 있습니까?
user541686

1
@BobTurbo : 45983493은 12 시간 이상입니다. 오타입니까?
거스

1
좋아, 이제 다시 가야한다 :)
BobTurbo

7

MSVC와 같은 우수한 최적화 컴파일러는 다음을 생성 할 수 있습니다.

  1. 케이스가 좋은 장거리에 배치되면 간단한 점프 테이블
  2. 간격이 많은 경우 스파 스 (2 레벨) 점프 테이블
  3. 사례 수가 적거나 값이 서로 근접하지 않은 경우 일련의 if
  4. 경우가 근접한 범위의 여러 그룹을 나타내는 경우 위의 조합.

간단히 말해 스위치가 일련의 if보다 느리게 보일 경우 컴파일러는 스위치를 하나만 변환 할 수 있습니다. 그리고 그것은 각각의 경우에 대한 일련의 비교 일뿐만 아니라 이진 검색 트리 일 것입니다. 예를 보려면 여기 를 참조 하십시오 .


실제로 컴파일러는이를 해시 및 점프로 대체 할 수도 있는데, 이는 제안한 드문 2 단계 솔루션보다 성능이 우수합니다.
Alice

5

2)에 대답하고 일반적인 의견을 제시합니다. 2) 아니요, 게시 한 어셈블리 코드에 점프 테이블이 없습니다. 점프 테이블은 점프 대상 테이블과 테이블에서 인덱스 된 위치로 직접 점프하는 하나 또는 두 개의 명령어입니다. 가능한 전환 대상이 많은 경우 점프 테이블이 더 적합합니다. 아마도 옵티마이 저는 대상 수가 임계 값보다 크지 않으면 로직이 더 빠르면 단순하다는 것을 알고있을 것입니다. 4가 아닌 20 가지 가능성으로 다시 예를 들어보십시오.


# 2에 대한 답변에 +1 감사합니다! (:), BTW 여기 결과는 더 많은 가능성과 함께 있습니다.)
user541686

4

나는 흥미를 느꼈고 스위치 문을 더 빨리 실행하기 위해 예제에서 무엇을 바꿀 수 있는지 살펴 보았습니다.

40 개의 if 문에 도달하고 0의 경우를 추가하면 if 블록은 동등한 switch 문보다 느리게 실행됩니다. https://www.ideone.com/KZeCz에 결과가 있습니다 .

0 건을 제거하면 https://www.ideone.com/LFnrX 에서 효과를 볼 수 있습니다 .


1
귀하의 링크가 고장났습니다.
TS

4

구 벤치 마크 벤치 마크 벤치 마크 결과는 다음과 같습니다.

Test Name:   F000003                         Class Name:  Style
CPU Time:       0.781  nanoseconds           plus or minus     0.0715
Wall/CPU:        1.00  ratio.                Iteration Count:  1677721600
Test Description:
 Time to test a global using a 2-way if/else if statement
 compare this test with F000004

Test Name:   F000004                         Class Name:  Style
CPU Time:        1.53  nanoseconds           plus or minus     0.0767
Wall/CPU:        1.00  ratio.                Iteration Count:  1677721600
Test Description:
 Time to test a global using a 2-way switch statement
 compare this test with F000003

Test Name:   F000005                         Class Name:  Style
CPU Time:        7.70  nanoseconds           plus or minus      0.385
Wall/CPU:        1.00  ratio.                Iteration Count:  1677721600
Test Description:
 Time to test a global using a 10-way if/else if statement
 compare this test with F000006

Test Name:   F000006                         Class Name:  Style
CPU Time:        2.00  nanoseconds           plus or minus     0.0999
Wall/CPU:        1.00  ratio.                Iteration Count:  1677721600
Test Description:
 Time to test a global using a 10-way switch statement
 compare this test with F000005

Test Name:   F000007                         Class Name:  Style
CPU Time:        3.41  nanoseconds           plus or minus      0.171
Wall/CPU:        1.00  ratio.                Iteration Count:  1677721600
Test Description:
 Time to test a global using a 10-way sparse switch statement
 compare this test with F000005 and F000006

우리가 볼 수있는 것은 (이 컴파일러에서 VC ++ 9.0 x64를 사용하는이 컴퓨터에서) 각 if테스트에는 약 0.7 나노 초가 걸립니다. 테스트 수가 증가함에 따라 시간은 거의 완벽하게 선형으로 확장됩니다.

switch 문 을 사용하면 값이 조밀하다면 2-way 테스트와 10-way 테스트간에 속도 차이 가 거의 없습니다. 희소 값을 가진 10-way 테스트는 밀도가 높은 10-way 테스트보다 약 1.6 배의 시간이 걸리지 만 희소 값을 사용하더라도 10-way if/ 속도의 두 배보다 여전히 낫습니다 else if.

결론 : 4 방향 테스트 만 사용하면 실제로 vs / 의 성능에 대해 많은 것을 보여주지는 않습니다 . 이 코드의 숫자를 보면 4 방향 테스트의 경우 두 가지가 매우 유사한 결과 ( /의 경우 ~ 2.8 나노초, /의 경우 ~ 2.0 ) 를 생성한다는 사실을 보간하기가 쉽습니다 .switchifelseifelseswitch


1
테스트가 의도적으로 일치하지 않는 값을 찾거나 if/ else체인 의 끝에서만 일치하는 값을 산란 시키는 지 알지 못하는 경우 어떻게 해야하는지 알기가 어렵습니다 . bench++10 후 소스를 찾을 수 없습니다. 분 인터넷 검색.
Tony Delroy

3

스위치가 점프 테이블로 컴파일되지 않은 경우 스위치보다 더 효율적인 경우 쓸 수 있습니다.

(1) 모든 N에 대해 최악의 테스트가 아닌 순서가있는 경우, 상위 또는 하위 절반에 있는지 테스트하고 if의 각 절반에 바이너리 검색 스타일을 쓸 수 있습니다. 최악의 경우는 N이 아닌 logN

(2) 특정 사례 / 그룹이 다른 사례보다 훨씬 더 빈번한 경우 해당 사례를 먼저 분리 할 경우를 설계하면 평균 시간을 단축 할 수 있습니다.


이것은 사실이 아닙니다. 컴파일러는 이러한 최적화를 모두 능가 할 수 있습니다.
Alice

1
Alice, 예상되는 워크로드에서 다른 케이스보다 어떤 케이스가 더 자주 발생하는지 컴파일러는 어떻게 알 수 있습니까? (A : 알 수 없으므로 최적화를 할 수 없습니다.)
Brian Kennedy

(1) 쉽게 이진 검색을 수행하여 쉽게 수행 할 수 있으며 일부 컴파일러에서 수행됩니다. (2) 다양한 방법으로 예측하거나 컴파일러에 지시 할 수 있습니다. GCC를 "거의"또는 "거의"사용하지 않은 적이 있습니까?
Alice

또한 일부 컴파일러는 통계를 수집 한 다음 해당 정보에서 최적화하는 모드로 프로그램을 실행할 수 있습니다.
Phil1970

2

점프 테이블은 주소 테이블을 가지거나 해시 또는 이와 유사한 것을 사용합니다.

더 빠르거나 느리게 주관적입니다. 예를 들어 사례 1을 첫 번째 대신 마지막으로 사용하고 테스트 프로그램 또는 실제 프로그램에서 사례 1을 대부분 사용하는 경우이 구현으로 코드가 느려질 수 있습니다. 따라서 구현에 따라 사례 목록을 다시 정렬하면 큰 차이를 만들 수 있습니다.

1-4 대신 0-3을 사용한 경우 컴파일러에서 점프 테이블을 사용했을 수 있습니다. 컴파일러는 어쨌든 +1을 제거해야합니다. 아마도 그것은 적은 수의 아이템이었을 것입니다. 예를 들어 0-15 또는 0-31로 만든 경우 테이블로 구현했거나 다른 바로 가기를 사용했을 수 있습니다. 컴파일러는 소스 코드의 기능을 충족하는 한 구현 방법을 자유롭게 선택할 수 있습니다. 그리고 이것은 컴파일러의 차이와 버전의 차이와 최적화의 차이를 가져옵니다. 점프 테이블을 원한다면 점프 테이블을 만들고 if-then-else 트리를 원한다면 if-then-else 트리를 만드십시오. 컴파일러가 결정하도록하려면 switch / case 문을 사용하십시오.


2

그러나 왜 하나가 더 빠르며 느린 지 잘 모르겠습니다.

실제로 설명하기가 너무 어렵지 않습니다 ... 잘못 예측 한 지점이 올바르게 예측 된 지점보다 수십에서 수백 배 더 비싸다는 것을 기억한다면

에서 % 20버전, 첫 번째 경우는 / 경우는 항상 안타 그 하나입니다. 최신 CPU는 일반적으로 사용되는 분기와 그렇지 않은 분기를 "학습"하므로이 분기가 거의 모든 루프 반복에서 어떻게 동작하는지 쉽게 예측할 수 있습니다. "if"버전이 비행하는 이유를 설명합니다. 첫 번째 테스트 이후에는 아무것도 실행할 필요가 없으며 대부분의 반복에 대해 해당 테스트의 결과를 (올바르게) 예측합니다. 분명히 "스위치"는 약간 다르게 구현 될 수 있습니다. 아마도 점프 테이블도 계산 분기로 인해 느려질 수 있습니다.

에서 % 21버전, 가지 본질적으로 무작위입니다. 따라서 많은 반복 작업을 수행 할 때마다 CPU가 실행될 방법을 추측 할 수 없습니다. 점프 테이블 (또는 다른 "스위치"최적화)이 도움이 될 수 있습니다.

최신 컴파일러 및 CPU로 코드 조각이 어떻게 수행되는지 예측하기가 매우 어렵고 모든 세대에서 더 어려워집니다. 최선의 충고는 "귀찮게하지 말고 항상 프로필하는 것"입니다. 그 조언은 더 나아지고, 그것을 무시할 수있는 사람들은 매년 더 작아집니다.

위의 설명은 대부분 추측입니다. :-)


2
수백 배 느려진 곳이 어디인지 알 수 없습니다. 잘못 예측 된 브랜치의 최악의 경우는 파이프 라인 스톨로, 대부분의 최신 CPU에서 ~ 20 배 느려질 것입니다. 수백 번이 아닙니다. (오래된 NetBurst 칩을 사용하는 경우 35 배 느려질 수 있습니다 ...)
Billy ONeal

@ 빌리 : 알았어요. 그래서 조금만 기대하고 있습니다. Sandy Bridge 프로세서 에서 "각각의 잘못 예측 된 지점은 전체 파이프 라인을 플러시하여 최대 100여 개의 기내 명령 작업을 잃게됩니다." 파이프 라인은 일반적으로 모든 세대에서 점점 더 깊어지고 있습니다.
Nemo

1
사실이 아니다. P4 (NetBurst)에는 31 개의 파이프 라인 단계가있었습니다. 샌디 브릿지는 무대가 상당히 적습니다. "100 개 정도의 명령을 잃어버린다"는 명령 캐시가 무효화된다는 가정하에 있다고 생각합니다. 실제로 발생하는 일반적인 간접 점프의 경우 점프 테이블과 같은 경우 간접 점프의 대상이 명령 캐시의 어딘가에있을 수 있습니다.
Billy ONeal

@ 빌리 : 우리가 동의하지 않는 것 같아요. 나의 진술은 "잘못 예측 된 브랜치가 정확하게 예측 된 브랜치보다 수십에서 수 백배 더 비싸다"는 것이었다. 아마도 약간의 과장일지도 모르겠지만 ... I- 캐시 및 실행 파이프 라인 깊이의 히트보다 더 많은 일이 진행되고 있습니다. 내가 읽은 것으로부터, 디코드 단독의 큐는 ~ 20 명령입니다.
니모

분기 예측 하드웨어가 실행 경로를 잘못 예측하는 경우 명령 파이프 라인에있는 잘못된 경로의 UOP는 실행 중단없이 해당 위치에서 간단하게 제거됩니다. 나는 아무 생각이 없다 방법 이 가능하다을 (또는 나는 그것을 잘못 해석하고있어 여부),하지만 분명히 존재하지 아니 네 할렘에서 잘못 예측 지사와 함께 파이프 라인 노점은? (다시, 나는 i7이 없다; 나는 i5가있다. 그래서 이것은 나의 경우에는 적용되지 않는다.)
user541686

1

없음 어셈블러로 들어가서 실제 성능을 측정하는 대부분의 경우 질문은 단순히 잘못된 것입니다. 주어진 예에서 당신의 생각은 결정적으로 너무 짧습니다.

counter += (4 - counter % 4);

당신이 사용해야하는 올바른 증분 표현으로 보입니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.