programing

왜 GCC는 거의 동일한 C코드에 대해 이렇게 근본적으로 다른 어셈블리를 생성합니까?

kingscode 2022. 7. 12. 23:45
반응형

왜 GCC는 거의 동일한 C코드에 대해 이렇게 근본적으로 다른 어셈블리를 생성합니까?

된 「 」를 .ftol는 매우 했습니다.GCC 4.6.1먼저 코드를 표시하겠습니다(명확하게 하기 위해 차이점을 표시했습니다).

fast_trunc_one, C:

int fast_trunc_one(int i) {
    int mantissa, exponent, sign, r;

    mantissa = (i & 0x07fffff) | 0x800000;
    exponent = 150 - ((i >> 23) & 0xff);
    sign = i & 0x80000000;

    if (exponent < 0) {
        r = mantissa << -exponent;                       /* diff */
    } else {
        r = mantissa >> exponent;                        /* diff */
    }

    return (r ^ -sign) + sign;                           /* diff */
}

fast_trunc_two, C:

int fast_trunc_two(int i) {
    int mantissa, exponent, sign, r;

    mantissa = (i & 0x07fffff) | 0x800000;
    exponent = 150 - ((i >> 23) & 0xff);
    sign = i & 0x80000000;

    if (exponent < 0) {
        r = (mantissa << -exponent) ^ -sign;             /* diff */
    } else {
        r = (mantissa >> exponent) ^ -sign;              /* diff */
    }

    return r + sign;                                     /* diff */
}

★★★★★★★★★★★★★★★★★?GCC의「 」로 한 후gcc -O3 -S -Wall -o test.s test.c어셈블리 출력은 다음과 같습니다.

fast_syslogc_one, 생성:

_fast_trunc_one:
LFB0:
    .cfi_startproc
    movl    4(%esp), %eax
    movl    $150, %ecx
    movl    %eax, %edx
    andl    $8388607, %edx
    sarl    $23, %eax
    orl $8388608, %edx
    andl    $255, %eax
    subl    %eax, %ecx
    movl    %edx, %eax
    sarl    %cl, %eax
    testl   %ecx, %ecx
    js  L5
    rep
    ret
    .p2align 4,,7
L5:
    negl    %ecx
    movl    %edx, %eax
    sall    %cl, %eax
    ret
    .cfi_endproc

fast_syslogc_2, 생성됨:

_fast_trunc_two:
LFB1:
    .cfi_startproc
    pushl   %ebx
    .cfi_def_cfa_offset 8
    .cfi_offset 3, -8
    movl    8(%esp), %eax
    movl    $150, %ecx
    movl    %eax, %ebx
    movl    %eax, %edx
    sarl    $23, %ebx
    andl    $8388607, %edx
    andl    $255, %ebx
    orl $8388608, %edx
    andl    $-2147483648, %eax
    subl    %ebx, %ecx
    js  L9
    sarl    %cl, %edx
    movl    %eax, %ecx
    negl    %ecx
    xorl    %ecx, %edx
    addl    %edx, %eax
    popl    %ebx
    .cfi_remember_state
    .cfi_def_cfa_offset 4
    .cfi_restore 3
    ret
    .p2align 4,,7
L9:
    .cfi_restore_state
    negl    %ecx
    sall    %cl, %edx
    movl    %eax, %ecx
    negl    %ecx
    xorl    %ecx, %edx
    addl    %edx, %eax
    popl    %ebx
    .cfi_restore 3
    .cfi_def_cfa_offset 4
    ret
    .cfi_endproc

그건 엄청난 차이네요.이건 프로파일에도 나타나는데fast_trunc_one% 빠르다보다 정도 빠릅니다.fast_trunc_two이제 질문하겠습니다.엇이이원?원 인??

OP 편집과 동기화하도록 업데이트됨

코드를 만지작거림으로써 GCC가 첫 번째 케이스를 어떻게 최적화하는지 알 수 있었다.

이렇게 GCC가 GCC를 최적화하는지 해야 합니다.fast_trunc_one().

말거나, 거나말 believe believe believe believe .fast_trunc_one()

int fast_trunc_one(int i) {
    int mantissa, exponent;

    mantissa = (i & 0x07fffff) | 0x800000;
    exponent = 150 - ((i >> 23) & 0xff);

    if (exponent < 0) {
        return (mantissa << -exponent);             /* diff */
    } else {
        return (mantissa >> exponent);              /* diff */
    }
}

.fast_trunc_one() 등 다 해 주세요 - 등록해 . - 등록해 주세요.

「」가해 주세요.xor" " " 의 어셈블리에 s가되어 있습니다.fast_trunc_one()그래서 그걸 줬어.


어떻게요?


순서 1: sign = -sign

그럼 먼저 ㅇㅇㅇ, ㅇㅇㅇ, ㅇㅇ, ㅇㅇ를 요?sign since. 터터가래sign = i & 0x80000000;는 두 수 있습니다.sign하다

  • sign = 0
  • sign = 0x80000000

그러면두가지경우,sign == -sign따라서 원래 코드를 다음과 같이 변경할 때:

int fast_trunc_one(int i) {
    int mantissa, exponent, sign, r;

    mantissa = (i & 0x07fffff) | 0x800000;
    exponent = 150 - ((i >> 23) & 0xff);
    sign = i & 0x80000000;

    if (exponent < 0) {
        r = mantissa << -exponent;
    } else {
        r = mantissa >> exponent;
    }

    return (r ^ sign) + sign;
}

은 원래의 것과 낸다.fast_trunc_one()조립은 생략하겠습니다만, 동일합니다.★★★★★★★★★★★★★★★★★★★★★★★★★★★


2단계: 수학적 감소:x + (y ^ x) = y

sign는 두 값 중 할 수 .0 ★★★★★★★★★★★★★★★★★」0x80000000.

  • x = 0 , , , 「 」x + (y ^ x) = y사소한 것이라도 버틸 수 있다.
  • xering에 의한 및 0x80000000똑같아요.신호 비트를 뒤집습니다.따라서 '''는x + (y ^ x) = y 아,아,아,아,아,아,아,아,아,아,아,아,아,아,아,아,아,아,아,아,아,아.x = 0x80000000.

therefore그 、x + (y ^ x)y코드는 다음과 같이 단순화됩니다.

int fast_trunc_one(int i) {
    int mantissa, exponent, sign, r;

    mantissa = (i & 0x07fffff) | 0x800000;
    exponent = 150 - ((i >> 23) & 0xff);
    sign = i & 0x80000000;

    if (exponent < 0) {
        r = (mantissa << -exponent);
    } else {
        r = (mantissa >> exponent);
    }

    return r;
}

다시, 이것은 정확히 동일한 어셈블리(이름 등록 및 모두)로 컴파일됩니다.


위의 버전은 최종적으로 다음과 같이 축소됩니다.

int fast_trunc_one(int i) {
    int mantissa, exponent;

    mantissa = (i & 0x07fffff) | 0x800000;
    exponent = 150 - ((i >> 23) & 0xff);

    if (exponent < 0) {
        return (mantissa << -exponent);             /* diff */
    } else {
        return (mantissa >> exponent);              /* diff */
    }
}

GCC가 어셈블리에서 생성하는 것과 거의 일치합니다.


왜 죠?fast_trunc_two()★★★★★★★★★★★★★★★★?

fast_trunc_one()는 는 입니다.x + (y ^ x) = y★★★★★fast_trunc_two()x + (y ^ x)브런치 전체에서 식을 분할하고 있습니다.

GCC는 GCC를 사용합니다 (GCC)를 들어올릴 )^ -sign 합치다.r + sign( 막た )

예를 들어, 이렇게 하면 다음과 같은 어셈블리가 생성됩니다.fast_trunc_one():

int fast_trunc_two(int i) {
    int mantissa, exponent, sign, r;

    mantissa = (i & 0x07fffff) | 0x800000;
    exponent = 150 - ((i >> 23) & 0xff);
    sign = i & 0x80000000;

    if (exponent < 0) {
        r = ((mantissa << -exponent) ^ -sign) + sign;             /* diff */
    } else {
        r = ((mantissa >> exponent) ^ -sign) + sign;              /* diff */
    }

    return r;                                     /* diff */
}

이것이 컴파일러의 특성입니다.그들이 가장 빠르거나 최선의 길을 택할 것이라고 가정하는 것은 완전히 잘못된 것이다."현대 컴파일러"가 빈칸을 채우고, 최고의 작업을 수행하고, 가장 빠른 코드를 만들기 위해 코드를 최적화할 필요가 없다는 것을 암시하는 사람.실제로 암에서는 적어도 3.x에서 4.x로 gcc가 악화되는 것을 보았습니다. 4.x는 이 시점까지 3.x를 따라잡았을지도 모릅니다만, 초기에는 더 느린 코드가 생성되었습니다.연습하면 컴파일러가 더 열심히 작업하지 않아도 되고 결과적으로 더 일관되고 기대되는 결과를 얻을 수 있도록 코드를 작성하는 방법을 배울 수 있습니다.

여기서의 버그는 실제 생산되는 것이 아니라 앞으로 생산될 것에 대한 당신의 기대입니다.컴파일러가 같은 출력을 생성하도록 하려면 같은 입력을 입력합니다.수학적으로도 같지도 않고, 같은 종류도 아니고, 실제로도 같지도 않고, 다른 경로도 없고, 한 버전에서 다른 버전으로 공유하거나 배포하는 작업도 없습니다.이것은 코드 작성 방법과 컴파일러가 코드 작성 방법을 이해하는 데 도움이 됩니다.1개의 프로세서 타겟에 대해 1개의 버전의 gcc가 어느 날 특정 결과가 나왔기 때문에 그것이 모든 컴파일러와 모든 코드의 규칙이라고 착각하지 마십시오.진행 상황을 파악하려면 많은 컴파일러와 대상을 사용해야 합니다.

gcc는 꽤 지저분합니다.커튼 뒤나 gcc의 내장을 보고 타겟을 추가하거나 직접 수정해 보시기 바랍니다.덕트 테이프와 베일링 와이어로 간신히 고정되어 있습니다.중요한 장소에서 추가 또는 제거된 코드 행이 무너져 내립니다.왜 다른 기대에 부응하지 못했는지 걱정할 필요 없이 사용 가능한 코드를 전혀 생성했다는 것은 기쁜 일이다.

gcc의 다른 버전이 무엇을 생성하는지 보셨습니까? 3.x와 4.x는 특히 4.5 대 4.6 대 4.7 등이며, 다른 타겟 프로세서, x86, 암, mips 등 또는 사용하는 네이티브 컴파일러가 32비트 대 64비트 등 다양한 맛의 x86에 대해 알아보셨습니까?그리고 다른 타겟에 대해 llvm(clang)을 클릭합니다.

신비주의자는 코드 분석/최적화 문제를 해결하는 데 필요한 사고 과정을 훌륭하게 수행했고, 컴파일러가 어떤 "현대 컴파일러"도 기대하지 않는 어떤 것도 내놓기를 기대했다.

수학 속성, 이 양식의 코드를 입력하지 않고

if (exponent < 0) {
  r = mantissa << -exponent;                       /* diff */
} else {
  r = mantissa >> exponent;                        /* diff */
}
return (r ^ -sign) + sign;                           /* diff */

컴파일러를 A로 유도합니다.이 형식으로 구현하고 if-then-else를 실행한 후 공통 코드로 수렴하여 종료하고 돌아갑니다.또는 B: 이것은 함수의 끝이기 때문에 브랜치를 저장합니다.또한 r을 사용하거나 저장할 필요가 없습니다.

if (exponent < 0) {
  return((mantissa << -exponent)^-sign)+sign;
} else {
  return((mantissa << -exponent)^-sign)+sign;
}

그러면 Mystrict가 지적한 대로 코드 변수가 모두 사라집니다.컴파일러가 부호변수가 없어지는 것을 볼 수 있을 것이라고는 생각하지 않기 때문에, 컴파일러에게 억지로 알아내려고 하지 말고, 직접 해 주었으면 합니다.

이것은 gcc 소스 코드를 조사할 절호의 기회입니다.옵티마이저가 한 케이스에서 한 가지를 보고 다른 케이스에서 다른 것을 본 사례를 찾은 것 같습니다.그럼 다음 단계로 넘어가서 gcc가 그 케이스를 볼 수 없는지 확인해 보십시오.모든 최적화가 이루어지는 것은 일부 개인이나 그룹이 최적화를 인식하고 의도적으로 배치했기 때문입니다.이 최적화를 실현하기 위해, 누군가가 그것을 도입할 때마다(그 후 테스트해, 장래에 걸쳐서 유지 보수) 작업을 실시합니다.

코드 수가 적을수록 더 빠르고 더 많을수록 더 느리다고 가정하지 마십시오. 그렇지 않은 예를 만들고 찾는 것은 매우 쉽습니다.많은 코드보다 적은 코드가 더 빠른 경우가 많습니다.처음부터 설명했듯이, 그 경우 분기나 루프 등을 저장하기 위해 더 많은 코드를 생성하여 더 빠른 코드를 얻을 수 있습니다.

결론은 컴파일러에 다른 소스를 공급하고 같은 결과를 기대했다는 것입니다.문제는 컴파일러 출력이 아니라 사용자의 기대입니다.특정 컴파일러 및 프로세서에 대해 기능 전체를 극적으로 느리게 만드는 코드 한 줄을 추가하는 것을 시연하는 것은 매우 쉽습니다.예를 들어, a = b + 2; a = b + c + 2; 원인 _fill_in_the_blank_blank_deblank_name_로 변경하면 근본적으로 다르고 느린 코드가 생성되는 이유는 무엇입니까?물론 컴파일러의 답변은 입력에 다른 코드가 입력되어 있기 때문에 컴파일러가 다른 출력을 생성하는 것은 완전히 유효합니다.(관련되지 않은 두 줄의 코드를 교환하여 출력을 크게 변경하는 것이 더 좋습니다.)입력의 복잡성과 크기 사이에는 출력의 복잡성과 크기 사이에 예상되는 관계가 없습니다.다음과 같은 것을 쨍그랑에 넣습니다.

for(ra=0;ra<20;ra++) dummy(ra);

60-100라인의 조립업체를 생산했습니다.루프가 풀렸어요회선을 세지 않았습니다.생각해보면 함수 호출에 대한 입력에 결과를 복사하고 함수 호출을 3회 이상 수행해야 합니다.따라서 60개 이상의 명령어(루프당 4개일 경우 80개, 루프당 5개일 경우 100개 등)에 따라 달라집니다.

Mysticial은 이미 훌륭한 설명을 해줬지만, 왜 컴파일러가 다른 컴파일러가 아닌 다른 컴파일러에 최적화를 하는지 근본적인 것은 없다고 덧붙였습니다.

LLVM †clang예를 들어 컴파일러는 두 함수에 대해 동일한 코드(함수 이름 제외)를 부여하며 다음을 제공합니다.

_fast_trunc_two:                        ## @fast_trunc_one
        movl    %edi, %edx
        andl    $-2147483648, %edx      ## imm = 0xFFFFFFFF80000000
        movl    %edi, %esi
        andl    $8388607, %esi          ## imm = 0x7FFFFF
        orl     $8388608, %esi          ## imm = 0x800000
        shrl    $23, %edi
        movzbl  %dil, %eax
        movl    $150, %ecx
        subl    %eax, %ecx
        js      LBB0_1
        shrl    %cl, %esi
        jmp     LBB0_3
LBB0_1:                                 ## %if.then
        negl    %ecx
        shll    %cl, %esi
LBB0_3:                                 ## %if.end
        movl    %edx, %eax
        negl    %eax
        xorl    %esi, %eax
        addl    %edx, %eax
        ret

이 코드는 OP의 첫 번째 gcc 버전만큼 짧지는 않지만 두 번째 버전만큼 길지는 않습니다.

다른 컴파일러의 코드(이름을 붙이지 않음)는 x86_64용으로 컴파일하여 두 함수에 대해 다음과 같이 생성됩니다.

fast_trunc_one:
        movl      %edi, %ecx        
        shrl      $23, %ecx         
        movl      %edi, %eax        
        movzbl    %cl, %edx         
        andl      $8388607, %eax    
        negl      %edx              
        orl       $8388608, %eax    
        addl      $150, %edx        
        movl      %eax, %esi        
        movl      %edx, %ecx        
        andl      $-2147483648, %edi
        negl      %ecx              
        movl      %edi, %r8d        
        shll      %cl, %esi         
        negl      %r8d              
        movl      %edx, %ecx        
        shrl      %cl, %eax         
        testl     %edx, %edx        
        cmovl     %esi, %eax        
        xorl      %r8d, %eax        
        addl      %edi, %eax        
        ret                         

을 모두 입니다.if마지막에는 조건부 동작을 사용하여 적절한 동작을 선택합니다.

Open64 컴파일러는 다음을 생성합니다.

fast_trunc_one: 
    movl %edi,%r9d                  
    sarl $23,%r9d                   
    movzbl %r9b,%r9d                
    addl $-150,%r9d                 
    movl %edi,%eax                  
    movl %r9d,%r8d                  
    andl $8388607,%eax              
    negl %r8d                       
    orl $8388608,%eax               
    testl %r8d,%r8d                 
    jl .LBB2_fast_trunc_one         
    movl %r8d,%ecx                  
    movl %eax,%edx                  
    sarl %cl,%edx                   
.Lt_0_1538:
    andl $-2147483648,%edi          
    movl %edi,%eax                  
    negl %eax                       
    xorl %edx,%eax                  
    addl %edi,%eax                  
    ret                             
    .p2align 5,,31
.LBB2_fast_trunc_one:
    movl %r9d,%ecx                  
    movl %eax,%edx                  
    shll %cl,%edx                   
    jmp .Lt_0_1538                  

"동일하지 입니다.fast_trunc_two.

어쨌든 최적화에 관한 한, 그것은 복권입니다.그것은 바로...특정 방식으로 코드가 컴파일되는 이유를 알기란 쉽지 않습니다.

언급URL : https://stackoverflow.com/questions/10250419/why-does-gcc-generate-such-radically-different-assembly-for-nearly-the-same-c-co

반응형