C 컴파일러가 스위치를 최적화하는 이유와 다르게


9

최근에 이상한 문제가 발생했을 때 개인 프로젝트를 진행하고있었습니다.

매우 엄격한 루프에서는 0과 15 사이의 값을 가진 정수가 있습니다. 0, 1, 8 및 9의 경우 -1을 얻고 값 4, 5, 12 및 13의 경우 1을 가져와야합니다.

나는 godbolt를 사용하여 몇 가지 옵션을 확인했으며 컴파일러가 if 체인과 같은 방식으로 switch 문을 최적화 할 수없는 것처럼 보였습니다.

링크는 다음과 같습니다 : https://godbolt.org/z/WYVBFl

코드는 다음과 같습니다

const int lookup[16] = {-1, -1, 0, 0, 1, 1, 0, 0, -1, -1, 0, 0, 1, 1, 0, 0};

int a(int num) {
    return lookup[num & 0xF];
}

int b(int num) {
    num &= 0xF;

    if (num == 0 || num == 1 || num == 8 || num == 9) 
        return -1;

    if (num == 4 || num == 5 || num == 12 || num == 13)
        return 1;

    return 0;
}

int c(int num) {
    num &= 0xF;
    switch (num) {
        case 0: case 1: case 8: case 9: 
            return -1;
        case 4: case 5: case 12: case 13:
            return 1;
        default:
            return 0;
    }
}

b와 c가 동일한 결과를 얻을 것이라고 생각했을 때 솔루션 (스위치 양식-다른 형식)이 상당히 느리기 때문에 효율적인 구현을 위해 비트 해킹을 읽을 수 있기를 바랐습니다.

이상하게도, b비트 해킹으로 컴파일되는 동안 c거의 최적화되지 않았거나 a대상 하드웨어 에 따라 다른 경우로 축소되었습니다 .

왜 이러한 불일치가 있는지 설명 할 수 있습니까? 이 쿼리를 최적화하는 '올바른'방법은 무엇입니까?

편집하다:

설명

내가 원하는 스위치 솔루션은 빠른, 또는 유사하게 "깨끗한"해결책이 될 수 있습니다. 그러나 내 컴퓨터에서 최적화로 컴파일하면 if 솔루션이 훨씬 빠릅니다.

시연을위한 빠른 프로그램을 작성했으며 TIO는 로컬에서 찾은 것과 동일한 결과를 얻 습니다.

static inline룩업 테이블을 사용 하면 속도가 약간 빨라집니다. 온라인으로 사용해보십시오!


4
대답은 "컴파일러가 항상 제정신의 선택을하는 것은 아닙니다"라고 생각합니다. 방금 GCC 8.3.0을 사용하여 코드를 객체로 -O3컴파일 c했으며 a또는 보다 나쁘거나 b( c두 개의 조건부 점프와 약간의 비트 조작, 단 하나의 조건부 점프와 간단한 비트 조작 b)했습니다. 항목 별 테스트보다 순진한 항목보다 낫습니다. 나는 당신이 정말로 무엇을 요구하는지 잘 모르겠습니다. 단순한 사실은 최적화 컴파일러가 설정할 수 있다는 것입니다 어떤 으로 이들을 어떤 이 경우이 선택하는 그래서 다른 사람의, 그것은 또는하지 않을 것입니다 무엇에 대한 하드 및 빠른 규칙이 없다.
ShadowRanger

내 문제는 빠른 것이 필요하지만 if 솔루션을 과도하게 유지 관리 할 수 ​​없다는 것입니다. 컴파일러가보다 깨끗한 솔루션을 충분히 최적화 할 수있는 방법이 있습니까? 이 경우 왜 그렇게 할 수 없는지 아무도 설명 할 수 없습니까?
LambdaBeta

나는 적어도 함수를 정적 또는 심지어 더 나은 인라인 으로 정의 하여 시작 합니다.
wildplasser

@wildplasser 속도 그것을 않지만, if여전히 박동 switch(이상한 조회도 빨라집니다) TIO 따를]
LambdaBeta

@LambdaBeta 컴파일러에게 특정 방식으로 최적화하도록 지시 할 방법이 없습니다. clang과 msvc는 이것에 대해 완전히 다른 코드를 생성합니다. 신경 쓰지 않고 gcc에서 가장 잘 작동하는 것을 원한다면 선택하십시오. 컴파일러 최적화는 휴리스틱을 기반으로하며 모든 경우에 최적의 솔루션을 제공하지는 않습니다. 그들은 모든 경우에 최적이 아닌 평균적인 경우에 좋으려고 노력하고 있습니다.
큐빅

답변:


6

모든 경우를 명시 적으로 열거하면 gcc가 매우 효율적입니다.

int c(int num) {
    num &= 0xF;
    switch (num) {
        case 0: case 1: case 8: case 9: 
            return -1;
        case 4: case 5: case 12: case 13:
            return 1;
            case 2: case 3: case 6: case 7: case 10: case 11: case 14: case 15: 
        //default:
            return 0;
    }
}

간단한 색인 분기에서 컴파일되었습니다.

c:
        and     edi, 15
        jmp     [QWORD PTR .L10[0+rdi*8]]
.L10:
        .quad   .L12
        .quad   .L12
        .quad   .L9
        .quad   .L9
        .quad   .L11
        .quad   .L11
        .quad   .L9
        .quad   .L9
        .quad   .L12
etc...

경우에하는 것으로 default:주석이며, GCC의 회전은 중첩 가지 버전으로 백업 할 수 있습니다.


1
@LambdaBeta 현대 인텔 CPU는 2 개의 병렬 인덱스 메모리 읽기 / 사이클을 수행 할 수있는 반면, 내 트릭의 처리량은 1 조회 / 사이클 일 수 있기 때문에 내 대답을 수락하지 않고 이것을 수락하는 것을 고려해야합니다. 플립 측면에서, 아마도 내 해킹 SSE2와 4 방향 벡터화 더 많은 의무가 pslld/ psrad또는 8 방향 AVX2 등가물. 코드의 다른 특성에 따라 다릅니다.
Iwillnotexist Idonotexist

4

C 컴파일러는 switch프로그래머가 이의 관용구를 이해 switch하고 악용 하기를 기대하기 때문에 특별한 경우가 있습니다.

다음과 같은 코드 :

if (num == 0 || num == 1 || num == 8 || num == 9) 
    return -1;

if (num == 4 || num == 5 || num == 12 || num == 13)
    return 1;

유능한 C 코더의 검토를 통과하지 못합니다. 서너 명의 검토자가 동시에 "이것은 switch!"

C 컴파일러가 if점프 테이블로 변환하기위한 명령문 구조를 분석하는 것은 가치가 없습니다 . 그 조건은 옳 아야하며 많은 if진술 에서 가능한 변이의 양은 천문학적입니다. 분석은 복잡 하고 부정적인 결과를 낳을 수 있습니다 ( "아니요,이 ifs를 switch" 로 변환 할 수 없습니다 ").


나는 그것이 스위치로 시작한 이유입니다. 그러나 필자의 경우 if 솔루션이 훨씬 빠릅니다. 나는 기본적으로 컴파일러가 스위치에서 더 나은 솔루션을 사용하도록 설득 할 수있는 방법이 있는지 묻고 있습니다. 스위치에서 ifs에서 패턴을 찾을 수 있었기 때문입니다. (만약 명확하지 않거나 유지 관리가 불가능하기 때문에 ifs는 특히 마음에 들지 않습니다)
LambdaBeta

정서가 있기 때문에 찬성했지만 받아 들여지지 않았기 때문에 정확하게이 질문을 한 것입니다. 내가 원하는 스위치를 사용하지만 내 경우에는 너무 느립니다, 나는 피하려고 if가능한 모든에 경우.
LambdaBeta

@LambdaBeta : 조회 테이블을 피해야 할 이유가 있습니까? 그것을 확인 static하고, C99를 지정 이니셜 라이저를 사용 하면 조금 더 무엇을 당신에게있는 거 할당을 취소 할 것인지, 그리고 그것을 완벽하게 정상적으로 분명합니다.
ShadowRanger

1
최적화 프로그램이해야 할 작업이 적도록 최소 비트를 폐기하는 것이 좋습니다.
R .. GitHub 중지 지원 얼음

@ShadowRanger 불행히도 여전히 if( 보다 느리게 편집됩니다). @R .. 컴파일러에 대한 전체 비트 단위 솔루션을 개발했습니다. 이제는 현재 사용중인 것입니다. 불행히도 필자의 경우 이것은 enum정수가 아닌 값이므로 비트 단위 해킹은 유지 관리가 쉽지 않습니다.
LambdaBeta

4

다음 코드는 ~ 3 클럭 사이클, ~ 4 유용한 명령어 및 ~ 13 바이트의 고 inline가용성 x86 머신 코드 에서 룩업 브랜치 프리, LUT 프리를 계산합니다 .

2의 보수 정수 표현에 따라 다릅니다.

그러나 u32s32typedef가 실제로 부호없는 32 비트 부호있는 정수 유형을 가리키는 지 확인해야합니다 . stdint.h유형 uint32_tint32_t적합했지만 헤더를 사용할 수 있는지 모르겠습니다.

const int lookup[16] = {-1, -1, 0, 0, 1, 1, 0, 0, -1, -1, 0, 0, 1, 1, 0, 0};

int a(int num) {
    return lookup[num & 0xF];
}


int d(int num){
    typedef unsigned int u32;
    typedef signed   int s32;

    // const int lookup[16]     = {-1, -1, 0, 0, 1, 1, 0, 0, -1, -1, 0, 0, 1, 1, 0, 0};
    // 2-bit signed 2's complement: 11 11 00 00 01 01 00 00 11 11 00 00 01 01 00 00
    // Hexadecimal:                   F     0     5     0     F     0     5     0
    const u32 K = 0xF050F050U;

    return (s32)(K<<(num+num)) >> 30;
}

int main(void){
    for(int i=0;i<16;i++){
        if(a(i) != d(i)){
            return !0;
        }
    }
    return 0;
}

https://godbolt.org/z/AcJWWf 에서 직접 참조하십시오.


상수 선택시

조회는 -1과 +1 사이의 16 개의 매우 작은 상수에 대한 것입니다. 각 비트는 2 비트 내에 들어가고 16 비트가 있으며 다음과 같이 배치 할 수 있습니다.

// const int lookup[16]     = {-1, -1, 0, 0, 1, 1, 0, 0, -1, -1, 0, 0, 1, 1, 0, 0};
// 2-bit signed 2's complement: 11 11 00 00 01 01 00 00 11 11 00 00 01 01 00 00
// Hexadecimal:                   F     0     5     0     F     0     5     0
u32 K = 0xF050F050U;

가장 중요한 비트에 가장 가까운 인덱스 0으로 배치하면 한 번만 시프트 2*num하면 2 비트 숫자의 부호 비트가 레지스터의 부호 비트에 배치됩니다. 2 비트 숫자를 32-2 = 30 비트만큼 오른쪽으로 이동하면 부호를 완전하게 확장 int하여 트릭을 완료합니다.


이것은 magic재생성 방법을 설명하는 주석 과 함께 가장 깨끗한 방법 일 수 있습니다. 어떻게 생각해 냈는지 설명해 주시겠습니까?
LambdaBeta

빠르면서도 깨끗하게 할 수 있기 때문에 받아 들여집니다. (일부 전 처리기 마술을 통해 :) < xkcd.com/541 >)
LambdaBeta

1
내 무점포 시도를 친다 :!!(12336 & (1<<x))-!!(771 & (1<<x));
technosaurus

0

산술 만 사용하여 동일한 효과를 만들 수 있습니다.

// produces : -1 -1 0 0 1 1 0 0 -1 -1 0 0 1 1 0 0 ...
int foo ( int x )
{
    return 1 - ( 3 & ( 0x46 >> ( x & 6 ) ) );
}

비록 기술적으로도 이것은 여전히 ​​(비트 단위) 조회입니다.

위의 내용이 너무 이상해 보이는 경우 다음을 수행 할 수도 있습니다.

int foo ( int x )
{
    int const y = x & 6;
    return (y == 4) - !y;
}
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.