c - 확인하지 - 어셈블리가 필수 구성 요소로 표시되려면 강력하게 서명되어야 합니다




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

Mysticial은 이미 큰 설명을 해주었습니다.하지만 FWIW를 추가하면 컴파일러가 왜 다른 컴파일러가 아닌 하나의 최적화를 만들지에 대한 근본적인 요소가 없다고 생각했습니다.

예를 들어, 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 와 유사하지만 유사하지는 않습니다.

어쨌든 최적화에 관해서는 복권입니다 - 그것이 무엇인지 ... 항상 코드가 왜 특정한 방식으로 컴파일되는지 알기가 쉽지 않습니다.

최적화 된 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_trunc_one, generated :

_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_trunc_two, generated :

_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_onefast_trunc_two 보다 약 30 % 빠릅니다. 이제 내 질문 :이 원인이 무엇입니까?


이것은 컴파일러의 특성입니다. 그들이 가장 빠르거나 최선의 길을 택한다고 가정하면, 이는 사실이 아닙니다. "현대 컴파일러"가 빈칸을 채우고, 최선을 다하고, 가장 빠른 코드를 작성하기 때문에 최적화를 위해 코드를 수행 할 필요가 없다는 것을 의미하는 사람은 누구나 gcc가 3.x에서 4로 악화되는 것을 보았습니다. x 적어도 팔에. 4.x는이 시점까지는 3.x에이를 수 있었지만 초기에는 느린 코드를 생성했습니다. 연습을하면 코드를 작성하는 방법을 배울 수 있으므로 컴파일러는 열심히 노력해야하며 결과적으로보다 일관되고 기대되는 결과를 얻을 수 있습니다.

여기서 벌어지는 것은 실제 생산 된 것이 아니라 생산 될 것에 대한 여러분의 기대입니다. 컴파일러에서 동일한 출력을 생성하도록하려면 동일한 입력을 피드하십시오. 수학적으로 같지 않고 동일하지는 않지만 실제로는 동일하거나 전혀 다른 경로가 아니며 한 버전에서 다른 버전으로 작업을 공유하거나 배포하지 않습니다. 이것은 코드를 작성하는 방법과 컴파일러가 코드를 사용하여 수행하는 방법을 이해하는 데 좋은 연습입니다. 하나의 프로세서 타겟을위한 gcc의 한 버전이 언젠가 모든 컴파일러와 모든 코드에 대한 규칙 인 특정 결과를 산출했기 때문에 실수를 저 지르지 마십시오. 당신은 무슨 일이 일어나고 있는지 느낄 수 있도록 많은 컴파일러와 많은 타겟을 사용해야합니다.

gcc는 꽤 심해서 커튼 뒤에서 gcc를 보거나 타겟을 추가하거나 직접 수정 해 보라고 권유합니다. 그것은 덕트 테이프와 베일 링 와이어 (bailing wire)로 간신히 묶여 있습니다. 중요한 곳에서 코드를 추가하거나 제거하면 여분의 코드 줄이 무너져 버립니다. 쓸모있는 코드를 생산했다는 사실은 다른 기대를 충족시키지 못한 이유에 대해 걱정하지 않고 기쁘게할만한 것입니다.

gcc의 다른 버전이 무엇을 만드는지 보셨습니까? 3.x 및 4.x 특히 4.5 대 4.6 대 4.7 등? 다른 타겟 프로세서, x86, arm, mips 등 또는 x86 컴파일러가 32 비트 대 64 비트 등의 다른 컴파일러라면? 그리고 다른 목표물에 대한 llvm (clang)?

Mystical은 코드 분석 / 최적화 문제를 해결하기 위해 필요한 사고 프로세스에서 탁월한 작업을 수행했으며, 컴파일러가 "현대 컴파일러"에 대해서는 예상하지 못했던 것을 기대합니다.

수학 속성에 들어 가지 않으면이 양식의 코드

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;
}

그런 다음 신비적으로 기호 변수가 모두 코드와 함께 사라질 것이라고 지적했습니다. 컴파일러가 부호 변수가 없어지는 것을 기대하지 않을 것이므로 컴파일러가 직접 알아 내야한다.

이것은 gcc 소스 코드를 파고 들기에 완벽한 기회입니다. 옵티마이 저가 한 사례에서 한 가지를 발견 한 다음 다른 사례에서 다른 사례를 발견 한 것으로 나타났습니다. 그런 다음 다음 단계로 가서 gcc가 그 사건을 볼 수 없는지 확인하십시오. 일부 개인 또는 그룹이 최적화를 인식하고이를 의도적으로 배치하기 때문에 모든 최적화가 이루어집니다. 이러한 최적화가 이루어지기 위해서는 누군가 그것을 배치하고 테스트 한 다음 미래에 유지해야 할 때마다 작업해야합니다.

확실히 적은 수의 코드가 더 빠르며 더 많은 코드가 더 느리다는 것을 가정하지 마십시오. 실제로 작성되지 않은 예제를 만들고 발견하는 것은 매우 쉽습니다. 더 적은 코드가 더 많은 코드보다 빠르다는 것보다 더 자주 발생합니다. 처음부터 시연했듯이, 더 많은 코드를 생성하여 그 경우 브랜칭을 저장하거나 루핑하는 등의 작업을 수행하고 그물 결과가 더 빠른 코드가되도록 할 수 있습니다.

결론은 컴파일러가 다른 소스를 제공하고 동일한 결과를 예상한다는 것입니다. 문제는 컴파일러 출력이 아니라 사용자의 기대입니다. 특정 컴파일러와 프로세서, 전체 기능을 크게 저하시키는 한 줄의 코드를 추가하는 방법을 설명하는 것은 매우 쉽습니다. 예를 들어 a = b + 2; a = b + c + 2; _fill_in_the_blank_compiler_name_가 근본적으로 다른 코드를 생성합니까? 물론 컴파일러가되는 대답은 입력에 다른 코드가 입력 되었기 때문에 컴파일러가 서로 다른 출력을 생성하는 것이 타당합니다. (두 개의 서로 관련이없는 코드 행을 교체하여 출력이 크게 변경되는 경우도 더 좋습니다.) 출력의 복잡성과 크기에 대한 입력의 복잡성과 크기 사이에는 예상되는 관계가 없습니다. clang에 다음과 같이 입력하십시오.

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

그것은 어셈블러의 60-100 라인 사이의 어딘가에서 생산되었습니다. 그것은 루프를 풀었다. 나는 당신이 그것에 대해 생각한다면 라인 수를 계산하지 않았고, 함수 호출에 대한 입력에 결과를 복사하고, 함수 호출을 호출하고, 최소 3 개의 연산을 추가해야한다. 목표에 따라 다르지만 아마 적어도 60 개 명령어, 루프 당 4 개라면 80 개, 루프 당 5 개이면 100 개 등입니다.





compiler-optimization