문제

GCC (보다 구체적으로 G ++)가 꼬리 재귀를 최적화하고 있는지 어떻게 알 수 있습니까? 특정 기능에서? (몇 번 등장했기 때문에 : GCC가 일반적으로 꼬리 재귀를 최적화 할 수 있는지 테스트하고 싶지 않습니다. 최적화 여부를 알고 싶습니다. 나의 꼬리 재귀 기능.)

귀하의 대답이 "생성 된 어셈블러를보십시오"라면, 내가 찾고있는 것을 정확히 알고 싶습니다. 그리고 최적화가 있는지 확인하기 위해 어셈블러를 검사하는 간단한 프로그램을 작성할 수 있는지 여부를 알고 싶습니다.

추신. 나는 이것이 질문의 일부로 보인다는 것을 알고 있습니다 C ++ 컴파일러가 테일 추방 최적화를하는 경우 어느 것이 있습니까? 5 개월 전부터. 그러나 나는 생각하지 않습니다 이 부분 그 질문 중 만족스럽게 답변되었습니다. (답은 "컴파일러가 최적화를했는지 확인하는 가장 쉬운 방법 (내가 알고있는)은 스택 오버플로를 초래하거나 어셈블리 출력을보고있는 호출을 수행하는 것입니다."

도움이 되었습니까?

해결책

사용합시다 다른 질문의 예제 코드. 컴파일하지만 GCC에게 조립하지 말라고 지시하십시오.

gcc -std=c99 -S -O2 test.c

이제 보자 _atoi 결과에서 기능 테스트 파일 (Mac OS 10.5의 GCC 4.0.1) :

        .text
        .align 4,0x90
_atoi:
        pushl   %ebp
        testl   %eax, %eax
        movl    %esp, %ebp
        movl    %eax, %ecx
        je      L3
        .align 4,0x90
L5:
        movzbl  (%ecx), %eax
        testb   %al, %al
        je      L3
        leal    (%edx,%edx,4), %edx
        movsbl  %al,%eax
        incl    %ecx
        leal    -48(%eax,%edx,2), %edx
        jne     L5
        .align 4,0x90
L3:
        leave
        movl    %edx, %eax
        ret

컴파일러는이 기능에서 꼬리 통화 최적화를 수행했습니다. 우리는 아니요가 없기 때문에 우리는 말할 수 있습니다 call 해당 코드의 명령은 원래 C 코드에 명확하게 함수 호출이있었습니다. 또한, 우리는 볼 수 있습니다 jne L5 함수에서 뒤로 점프하는 명령어는 C 코드에 루프가 분명히 없을 때 루프를 나타냅니다. 최적화로 다시 컴파일하면 꺼져 있으면 라인이 표시됩니다. call _atoi, 그리고 당신은 또한 뒤로 점프를 볼 수 없습니다.

자동화 할 수 있는지 여부는 또 다른 문제입니다. 어셈블러 코드의 세부 사항은 컴파일중인 코드에 따라 다릅니다.

당신은 그것을 프로그래밍 방식으로 발견 할 수 있다고 생각합니다. 함수가 스택 포인터의 현재 값을 인쇄하게하십시오 (x86에서 ESP 등록). 이 함수가 재귀 호출과 같은 첫 번째 호출에 대해 동일한 값을 인쇄하면 컴파일러가 꼬리 통화 최적화를 수행했습니다. 이 아이디어는 관찰하고자하는 기능을 수정해야하며, 이는 컴파일러가 기능을 최적화하기 위해 선택하는 방식에 영향을 줄 수 있습니다. 테스트가 성공하면 (두 번 동일한 ESP 값을 인쇄), 최적화가 계측없이 수행 될 것이라고 가정하는 것이 합리적이라고 생각하지만 테스트가 실패하면 실패가 계측 코드 추가.

다른 팁

편집하다 내 원래 게시물로 인해 GCC는 실제로 꼬리 통화 제거를 수행하지 못했습니다. 어쨌든 테일 콜 제거를 수행하기 위해 GCC를 바보로 바보 아래에 추가 속임수를 추가했습니다.

Steven의 답변을 확장하면 프로그래밍 방식으로 동일한 스택 프레임이 있는지 확인할 수 있습니다.

#include <stdio.h>

// We need to get a reference to the stack without spooking GCC into turning
// off tail-call elimination
int oracle2(void) { 
    char oracle; int oracle2 = (int)&oracle; return oracle2; 
}

void myCoolFunction(params, ..., int tailRecursionCheck) {
    int oracle = oracle2();
    if( tailRecursionCheck && tailRecursionCheck != oracle ) {
        printf("GCC did not optimize this call.\n");
    }
    // ... more code ...
    // The return is significant... GCC won't eliminate the call otherwise
    return myCoolFunction( ..., oracle);
}

int main(int argc, char *argv[]) {
    myCoolFunction(..., 0);
    return 0;
}

함수를 비상장으로 호출 할 때 0을 0으로 전달하십시오. 그렇지 않으면 오라클을 통과하십시오. 제거 해야하는 꼬리 재귀 호출이 없다면 런타임에 알려질 것입니다.

이것을 테스트 할 때, 내 버전의 GCC가 첫 번째 꼬리 통화를 최적화하지는 않지만 나머지 테일 호출은 최적화 된 것처럼 보입니다. 흥미로운.

생성 된 어셈블리 코드를보고 그것이 사용하는지 확인하십시오. call 또는 jmp X86의 재귀 호출에 대한 지침 (다른 아키텍처의 경우 해당 지침을 찾으십시오). 당신이 사용할 수있는 nm 그리고 objdump 기능에 해당하는 어셈블리 만 얻습니다. 다음 기능을 고려하십시오.

int fact(int n)
{
  return n <= 1 ? 1 : n * fact(n-1);
}

AS로 컴파일하십시오

gcc fact.c -c -o fact.o -O2

그런 다음 꼬리 재귀를 사용하는지 테스트하기 위해 :

# get starting address and size of function fact from nm
ADDR=$(nm --print-size --radix=d fact.o | grep ' fact$' | cut -d ' ' -f 1,2)
# strip leading 0's to avoid being interpreted by objdump as octal addresses
STARTADDR=$(echo $ADDR | cut -d ' ' -f 1 | sed 's/^0*\(.\)/\1/')
SIZE=$(echo $ADDR | cut -d ' ' -f 2 | sed 's/^0*//')
STOPADDR=$(( $STARTADDR + $SIZE ))

# now disassemble the function and look for an instruction of the form
# call addr <fact+offset>
if objdump --disassemble fact.o --start-address=$STARTADDR --stop-address=$STOPADDR | \
    grep -qE 'call +[0-9a-f]+ <fact\+'
then
    echo "fact is NOT tail recursive"
else
    echo "fact is tail recursive"
fi

위의 기능을 실행하면이 스크립트는 "사실은 재귀 적"인쇄를 인쇄합니다. 대신 컴파일 할 때 -O3 대신에 -O2,이 호기심으로 "사실은 꼬리 재귀가 아닙니다".

ehemient가 그의 의견에서 지적한 것처럼 이것은 잘못된 부정적인 점을 일으킬 수 있습니다. 이 스크립트는 함수에 전혀 재귀적인 호출이 포함되어 있지 않은 경우에만 정답을 얻을 수 있으며 형제 재귀 (예 : 어디에 A() 전화 B() 어떤 전화 A()). 현재 생성 된 어셈블리를 인간처럼 보지 않는 것이 더 강력한 방법을 생각할 수 없지만 적어도이 스크립트를 사용하여 객체 파일 내의 특정 함수에 해당하는 어셈블리를 쉽게 잡을 수 있습니다.

Polythinker의 답변을 확장하면 구체적인 예가 있습니다.

int foo(int a, int b) {
    if (a && b)
        return foo(a - 1, b - 1);
    return a + b;
}

i686-pc-linux-gnu-gcc-4.3.2 -Os -fno-optimize-sibling-calls 산출:

00000000 <foo>:
   0:   55                      push   %ebp
   1:   89 e5                   mov    %esp,%ebp
   3:   8b 55 08                mov    0x8(%ebp),%edx
   6:   8b 45 0c                mov    0xc(%ebp),%eax
   9:   85 d2                   test   %edx,%edx
   b:   74 16                   je     23 <foo+0x23>
   d:   85 c0                   test   %eax,%eax
   f:   74 12                   je     23 <foo+0x23>
  11:   51                      push   %ecx
  12:   48                      dec    %eax
  13:   51                      push   %ecx
  14:   50                      push   %eax
  15:   8d 42 ff                lea    -0x1(%edx),%eax
  18:   50                      push   %eax
  19:   e8 fc ff ff ff          call   1a <foo+0x1a>
  1e:   83 c4 10                add    $0x10,%esp
  21:   eb 02                   jmp    25 <foo+0x25>
  23:   01 d0                   add    %edx,%eax
  25:   c9                      leave
  26:   c3                      ret

i686-pc-linux-gnu-gcc-4.3.2 -Os 산출:

00000000 <foo>:
   0:   55                      push   %ebp
   1:   89 e5                   mov    %esp,%ebp
   3:   8b 55 08                mov    0x8(%ebp),%edx
   6:   8b 45 0c                mov    0xc(%ebp),%eax
   9:   85 d2                   test   %edx,%edx
   b:   74 08                   je     15 <foo+0x15>
   d:   85 c0                   test   %eax,%eax
   f:   74 04                   je     15 <foo+0x15>
  11:   48                      dec    %eax
  12:   4a                      dec    %edx
  13:   eb f4                   jmp    9 <foo+0x9>
  15:   5d                      pop    %ebp
  16:   01 d0                   add    %edx,%eax
  18:   c3                      ret

첫 번째 경우, <foo+0x11>-<foo+0x1d> 두 번째 경우에는 함수 호출에 대한 인수를 추진합니다. <foo+0x11>-<foo+0x14> 변수를 수정합니다 jmp서문 이후 어딘가에 같은 함수에 s. 그것이 당신이 찾고 싶은 것입니다.

나는 당신이 이것을 프로그래밍적으로 할 수 있다고 생각하지 않습니다. 가능한 변형이 너무 많습니다. 함수의 "고기"는 처음에 더 가까이 있거나 더 멀리 떨어져있을 수 있으며, 당신은 그것을 구별 할 수 없습니다. jmp 루프 나 조건부에서 보지 않고 조건부. a 대신 조건부 점프 일 수 있습니다 jmp. gcc a call 어떤 경우에는 다른 경우에 형제 통화 최적화를 적용하십시오.

FYI, GCC의 "형제 콜"은 테일 리커 징던 호출보다 약간 더 일반적입니다. 효과적으로 동일한 스택 프레임을 재사용하는 기능 호출은 잠재적으로 형제 자매 호출입니다.

편집하다

자립을 찾을 때의 예로 call 당신을 오도 할 것입니다.

int bar(int n) {
    if (n == 0)
        return bar(bar(1));
    if (n % 2)
        return n;
    return bar(n / 2);
}

GCC는 형제 콜 최적화를 3 개 중 2 개에 적용합니다. bar 전화. 나는 그것을 찾을 수 없지만 단일 최적화되지 않은 전화는 단일 레벨보다 더 이상 더 이상 진행되지 않기 때문에 여전히 그것을 최적화라고 부릅니다. call <bar+..> 생성 된 어셈블리에서.

나는 분해를보기에는 너무 게으르다. 이 시도:

void so(long l)
{
    ++l;
    so(l);
}
int main(int argc, char ** argv)
{
    so(0);
    return 0;
}

이 프로그램을 컴파일하고 실행하십시오. 그것이 영원히 실행되면, 꼬리 추방은 최적화되었습니다. 스택을 불고 있다면 그렇지 않았습니다.

편집 : 죄송합니다. 너무 빨리 읽으십시오. OP는 그의 특별한 기능 꼬리 재고가 최적화되어 있습니다. 확인...

... 원리는 여전히 동일합니다. 꼬리 수술이 최적화되면 스택 프레임은 동일하게 유지됩니다. 당신은 그것을 사용할 수 있어야합니다 백트레이스 기능 기능 내에서 스택 프레임을 캡처하고 성장 중인지 여부를 결정하십시오. 꼬리 재귀가 최적화되면 버퍼에서 하나의 리턴 포인터 만.

내가 확인한 또 다른 방법은 다음과 같습니다.

  1. 'gcc -o2'로 코드를 컴파일하십시오.
  2. 'GDB'시작
  3. 테일 추방/제거 될 것으로 예상되는 함수에 중단 점을 배치하십시오.
  4. 코드를 실행하십시오
  5. 꼬리 통화가 제거 된 경우, 중단 점은 한 번만 또는 전혀 맞지 않습니다. 이것에 대한 자세한 내용은 참조하십시오 이것

간단한 방법 : 간단한 꼬리 재귀 프로그램을 구축하고 컴파일 한 후 최적화되었는지 확인하십시오.

당신이 이미 당신의 질문에 그것을 가지고 있다는 것을 깨달았습니다. 어셈블리를 읽는 방법을 알고 있다면 말하기가 매우 쉽습니다. 재귀 함수는 기능 본문 내에서 스스로 ( "호출 레이블"포함)를 호출하고 루프는 "JMP 레이블"입니다.

최적화가 없으면 해당 기능 호출의 재귀가 너무 깊기 때문에 스택 오버플로로 이어지는 입력 데이터를 제작하고 발생하는지 확인할 수 있습니다. 물론 이것은 사소하지 않으며 때로는 충분히 큰 입력으로 인해 기능이 참을 수 없을 정도로 오랜 시간 동안 실행됩니다.

라이센스 : CC-BY-SA ~와 함께 속성
제휴하지 않습니다 StackOverflow
scroll top