메뉴 바로가기 검색 및 카테고리 바로가기 본문 바로가기

한빛출판네트워크

IT/모바일

GCC 파라메터와 친해지기(2)

한빛미디어

|

2007-05-30

|

by HANBIT

11,427

제공 : 한빛 네트워크
저자 : Mulyadi Santosa
역자 : 조성재
원문 : Getting Familiar with GCC Parameters

함수 호출과 관련된 옵션들

gcc는 기본적으로 여러분들에게 어떻게 함수를 불러올지 관리할 수 있는 여러 가지 방법들을 제공합니다. 먼저 인라인(inlining)에 대해 보도록 합니다. 인라인 적용에 의해 여러분들은 함수 호출 비용을 줄일 수 있습니다. 왜냐하면 함수의 실제 코드가 직접적으로 호출자의 위치에 붙기 때문입니다. 이것이 기본적으로 이뤄지는 것이 아니라는 것을 주의하여 주십시오. 여러분이 -O3 파라메터를 사용하거나 적어도 -finline-functions 를 사용할 때에만 적용됩니다.

gcc가 인라인을 적용하면 완성된 바이너리 코드는 어떻게 보일까요? [Listing 2]를 봅시다.
#include

inline test(int a, int b, int c)
{
        int d;
        d=a*b*c;
        printf("%d * %d * %d is %dn",a,b,c,d);
}

static inline test2(int a, int b, int c)
{
         int d;
         d=a+b+c;
         printf("%d + %d + %d is %dn",a,b,c,d);
}

int main(int argc, char *argv[])
{
        test(1,2,3);
        test2(4,5,6);
}
[Listing 2] 인라인이 적용된 소스

다음의 파라메터를 사용하여 [Listing 2]를 컴파일하십시오.
$ gcc -S -O3 -o  
-S는 gcc를 컴파일 단계 후에 바로 멈추도록 합니다. (우리는 이 부분을 나중에 다룰 것입니다.) 결과는 다음과 같습니다.
....
test:
        pushl   %ebp
        movl    %esp, %ebp
        pushl   %ebx
....
main:
        leal    4(%esp), %ecx
        andl    $-16, %esp
        pushl   -4(%ecx)
...
        movl    $6, 16(%esp)
        movl    $3, 12(%esp)
        movl    $2, 8(%esp)
        movl    $1, 4(%esp)
        movl    $.LC0, (%esp)
        call    printf
...
        movl    $15, 16(%esp)
        movl    $6, 12(%esp)
        movl    $5, 8(%esp)
        movl    $4, 4(%esp)
        movl    $.LC1, (%esp)
        call    printf
...
test() 와 test2() 양쪽 모두 인라인을 추가하지만, main() 바깥쪽에 남아있는 test()를 볼 것입니다. 이것은 규칙상 정적인 키워드(static keyword)가 위치하는 곳입니다. 함수를 static 이라고 말하는 것으로 여러분은 gcc가 이 함수를 어떤 다른 오브젝트 파일에 의해 호출할 것이라고 말한 것입니다. 따라서 이 코드를 일부러 생략할 필요가 없어졌습니다. 게다가 여러분이 이 함수를 언제나 사용 가능하도록 static으로 표시할 수 있다면 이 방법은 공간을 줄여줍니다. 반면에 어떤 함수를 인라인 적용할 것인지 결정할 때에는 현명해야 합니다. 작은 속도 성능을 위해 크기를 늘리는 것이 항상 가치가 있는 것은 아닙니다.

확실한 경험에 의한 권장안(heuristics)와 함께, gcc는 함수를 인라인 적용할 것인지, 말 것인지 결정합니다. 하나의 대안으로 가상 인스트럭션 용어에 함수 크기를 들 수 있습니다. 기본적으로 600이 한계입니다. 여러분은 이 한계를 -finline-limit를 통해 변경할 수 있습니다. 여러분의 경우에 더 나은 인라인 한계들을 찾기 위해 실험해보십시오. 경험에 의한 권장안을 덮어쓰는 것도 가능하며, 그로인해 gcc가 항상 함수를 인라인 적용할 수 있도록 합니다. 간단히 여러분의 함수를 다음과 같이 정의하십시오.
__attribute__((always_inline)) static inline test(int a, int b, int c)
이제 파라메터 전달을 합니다. x86 구조에서 파라메터들은 스택에 저장(push)되었다가 더 나은 처리를 하기 위해 함수 안에서 불러옵니다(pop). 그러나 gcc는 여러분들에게 이러한 동작을 레지스터로 대신 사용하도록 기회를 제공합니다. 3개 이상의 파라메터를 갖는 함수들은 이러한 요소들을 -mregparm= 을 전달하는 것으로 사용할 수 있습니다. 여기서 은 우리가 사용하기 원하는 레지스터의 수입니다. 만약 우리가 이 파라메터(n=3)를 Listing 2.에 적용한다면, inline 속성을 얻을 수 있으며, 아무런 최적화도 사용하지 않습니다. 우린 다음과 같은 소스를 얻습니다.
...
test:
        pushl   %ebp
        movl    %esp, %ebp
        subl    $56, %esp
        movl    %eax, -20(%ebp)
        movl    %edx, -24(%ebp)
        movl    %ecx, -28(%ebp)
...
main:
...
        movl    $3, %ecx
        movl    $2, %edx
        movl    $1, %eax
        call    test
스택 대신에 이것은 처음, 두 번째, 세 번째 파라메터를 기억해 두기 위해, EAX, EDX, 그리고 ECX를 사용합니다. 레지스터 접속 시간이 RAM보다 빠르기 때문이 이것은 실행시간을 줄이는 한가지 방법이 됩니다. 하지만 여러분들은 이 사항들을 주의하십시오.
  • 여러분은 모든 코드에 똑같은 -mregparm 레지스터 개수를 사용해야만 합니다. 그렇지 않다면 다른 소스가 다른 함수 호출 방법을 사용하도록 가정하게 된 후부터, 여러분이 다른 오브젝트 파일의 함수를 불러오는 경우 문제가 발생할 수 있습니다.
  • -mregparm을 사용하는 것으로 여러분은 기본적으로 Intel x86 호환 응용프로그램 바이너리 인터페이스(ABI)를 깨게 되는 것입니다. 그러므로 여러분은 여러분의 소프트웨어를 바이너리 형태로만 배포하는 경우 이 점을 명시해야만 합니다.
여러분은 아마도 모든 함수의 시작에서 다음 종류의 작업 순서를 인식하게 될 것입니다.
push   %ebp
mov    %esp,%ebp
sub    $0x28,%esp
이 작업 순서는 함수 프롤로그라고 알려져 있으며, 프레임 포인터(EBP)를 설정하기 위해 쓰여집니다. 이것으로 디버거가 스택 트레이스를 하는데 도움을 줍니다. 아래의 구조는 여러분들이 이것을 시각적으로 보는데 도움이 될 것입니다. [6]
[ebp-01] 마지막 지역 변수의 마지막 바이트
[ebp+00] 오래된 ebp 값
[ebp+04] 주소를 되돌려주기
[ebp+08] 첫 번째 인자
이것을 생략할 수 있을까요? 네, -fomit-frame-pointer 로, 함수 프롤로그를 단축하여 함수가 (지역변수가 있을 경우에만) 스택을 예약하는 것만으로 시작할 수 있습니다.
sub    $0x28,%esp
만약 함수가 자주 빈번하게 호출된다면, 프롤로그를 잘라내는 것으로 여러분의 프로그램에서 몇몇 CPU 동작을 줄입니다. 그러나 주의하십시오. 프롤로그를 잘라내는 것으로 여러분은 디버거가 스택 분석을 사용하기 힘들게 만들 것입니다. 예를 들어, test2 끝에 test(7,7,7)을 추가해보시고, -fomit-frame-pointer와 함께 최적화 파라메터 없이 다시 컴파일 해보십시오. 이제 gdb를 실행해서 바이너리 파일을 들여보십시오.
$ gdb inline
(gdb) break test
(gdb) r
Breakpoint 1, 0x08048384 in test ()
(gdb) cont
Breakpoint 1, 0x08048384 in test ()
(gdb) bt
#0  0x08048384 in test ()
#1  0x08048424 in test2 ()
#2  0x00000007 in ?? ()
#3  0x00000007 in ?? ()
#4  0x00000007 in ?? ()
#5  0x00000006 in ?? ()
#6  0x0000000f in ?? ()
#7  0x00000000 in ?? ()
두 번째 test 호출에서 프로그램은 멈추고 gdb는 스택 트레이스를 출력합니다. 보통 main()은 프레임 #2에 나타날 것입니다만, 우리는 물음표만 보게됩니다. 제가 스택 레이아웃에 관하여 언급했던 소스를 다시 호출하여 보십시오. 프레임 포인터의 부재가 gdb를 프레임 #2에 저장된 반환 주소의 위치를 찾지 못하도록 방해합니다.


역자 조성재님은 현재 오픈소스 데스크탑 환경인 Kool Desktop Environment (KDE) 프로젝트의 한국어 번역 코디네이터와 한국팀의 대표로 활동하고 있습니다.
TAG :
댓글 입력
자료실

최근 본 책0