gcc가 꼬리 재귀 최적화를 수행하는지 어떻게 확인합니까?
gcc (구체적으로는 g ++)가 특정 함수에서 꼬리 재귀 를 최적화하고 있는지 어떻게 알 수 있습니까? (이것은 몇 번을 마련이기 때문에 :. GCC는 일반적으로 꼬리 재귀를 최적화 할 수 있는지 테스트 싶지 않아 나는 그것이 최적화 알고 싶어 내 꼬리 재귀 함수를.)
귀하의 대답이 "생성 된 어셈블러보기"인 경우, 정확히 무엇을 찾고 있는지, 그리고 최적화가 있는지 확인하기 위해 어셈블러를 검사하는 간단한 프로그램을 작성할 수 있는지 여부를 알고 싶습니다.
추신. 이것이 꼬리 재귀 최적화를 수행하는 C ++ 컴파일러가있는 경우 질문의 일부로 나타나는 것을 알고 있습니다 . 5 개월 전부터. 그러나 나는 그 질문 의이 부분이 만족스럽게 대답되었다고 생각하지 않습니다 . (답은 "컴파일러가 최적화를 수행했는지 확인하는 가장 쉬운 방법은 (내가 알고있는) 스택 오버플로를 초래할 수있는 호출을 수행하거나 어셈블리 출력을 보는 것입니다.")
다른 질문의 예제 코드를 사용하겠습니다 . 컴파일하되 gcc에게 어셈블하지 말라고 지시하십시오.
gcc -std = c99 -S -O2 test.c
이제 _atoi
결과 test.s 파일 (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;
}
비재 귀적으로 함수를 호출하는 경우 check 매개 변수를 0으로 전달하십시오. 그렇지 않으면 오라클을 전달하십시오. 제거되어야하는 꼬리 재귀 호출이 그렇지 않은 경우 런타임에 알림을 받게됩니다.
이것을 테스트 할 때 내 버전의 GCC가 첫 번째 마무리 호출을 최적화하지 않는 것처럼 보이지만 나머지 마무리 호출은 최적화되어 있습니다. 흥미 롭군.
생성 된 어셈블리 코드를보고 x86의 재귀 호출에 대해 call
또는 jmp
명령어를 사용하는지 확인합니다 (다른 아키텍처의 경우 해당 명령어를 검색). nm
및 objdump
을 사용 하여 함수에 해당하는 어셈블리 만 가져올 수 있습니다 . 다음 기능을 고려하십시오.
int fact(int n)
{
return n <= 1 ? 1 : n * fact(n-1);
}
다음으로 컴파일
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
위의 함수에서 실행되면이 스크립트는 "fact is tail recursive"를 출력합니다. 대신 컴파일 할 때 -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 푸시 % ebp 1:89 e5 이동 % esp, % ebp 3 : 8b 55 08 mov 0x8 (% ebp), % edx 6 : 8b 45 0c mov 0xc (% ebp), % eax 9:85 d2 테스트 % edx, % edx b : 74 16 je 23 <foo + 0x23> d : 85 c0 테스트 % eax, % eax f : 74 12 je 23 <foo + 0x23> 11:51 % ecx 푸시 12:48 dec % eax 13:51 푸시 % ecx 14:50 % eax 푸시 15 : 8d 42 ff lea -0x1 (% edx), % eax 18:50 푸시 % eax 19 : e8 fc ff ff ff 호출 1a <foo + 0x1a> 1e : 83 c4 10 추가 $ 0x10, % esp 21 : eb 02 jmp 25 <foo + 0x25> 23:01 d0 % edx, % eax 추가 25 : c9 휴가 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
In the first case, <foo+0x11>-<foo+0x1d>
pushes arguments for a function call, while in the second case, <foo+0x11>-<foo+0x14>
modifies the variables and jmp
s to the same function, somewhere after the preamble. That's what you want to look for.
I don't think you can do this programatically; there's too much possible variation. The "meat" of the function may be closer to or further away from the start, and you can't distinguish that jmp
from a loop or conditional without looking at it. It might be a conditional jump instead of a jmp
. gcc
might leave a call
in for some cases but apply sibling call optimization to other cases.
FYI, gcc's "sibling calls" is slightly more general than tail-recursive calls -- effectively, any function call where re-using the same stack frame is okay is potentially a sibling call.
[edit]
As an example of when just looking for a self-recursive call
will mislead you,
int bar(int n) {
if (n == 0)
return bar(bar(1));
if (n % 2)
return n;
return bar(n / 2);
}
GCC will apply sibling call optimization to two out of the three bar
calls. I'd still call it tail-call-optimized, since that single unoptimized call never goes further than a single level, even though you'll find a call <bar+..>
in the generated assembly.
i am way too lazy to look at a disassembly. Try this:
void so(long l)
{
++l;
so(l);
}
int main(int argc, char ** argv)
{
so(0);
return 0;
}
compile and run this program. If it runs forever, the tail-recursion was optimized away. if it blows the stack, it wasn't.
EDIT: sorry, read too quickly, the OP wants to know if his particular function has its tail-recursion optimized away. OK...
...the principle is still the same - if the tail-recursion is being optimized away, then the stack frame will remain the same. You should be able to use the backtrace function to capture the stack frames from within your function, and determine if they are growing or not. If tail recursion is being optimized away, you will have only one return pointer in the buffer.
Another way I checked this is:
- Compile your code with 'gcc -O2'
- start 'gdb'
- Place a breakpoint in the function you are expecting to be tail-recursion optimized/eliminated
- run your code
- If it has been tail call eliminated, then the breakpoint will be hit only once or never. For more on this see this
A simple method: Build a simple tail recursion program, compile it, and dissemble it to see if it is optimized.
Just realized that you already had that in your question. If you know how to read assembly, it's quite easy to tell. Recursive functions will call themselves (with "call label") from within the function body, and a loop will be just "jmp label".
You could craft input data that would lead to stack overflow because of too deep recursion of that function calls if there were no optimization and see if it happens. Of course, this is not trivial and sometimes big enough inputs will make the function run for intolerably long period of time.
'Programing' 카테고리의 다른 글
브라우저 개발자 도구를 사용하여 브라우저가 사용하는 srcset 이미지를 볼 수 있습니까? (0) | 2020.12.05 |
---|---|
Java 9 컴파일러의 --release 플래그는 무엇입니까? (0) | 2020.12.05 |
기본 유형에 대한 일반 유형 제한을 정의하는 방법은 무엇입니까? (0) | 2020.12.05 |
Scala의 Product 클래스에 대해 어떻게 생각해야합니까? (0) | 2020.12.05 |
XML에 중첩 된 주석? (0) | 2020.12.05 |