프로파일링의 동작 원리는 프로그램내에 있는 각각의 함수들이 호출될 때, 호출된 지점에
대한 정보들을 보관해두도록 원래 코드에 프로파일링을 위한 내부 함수를 추가된
코드를 만들고 이 코드를 다시 컴파일하여 실행하는 것이다. 함수 호출시에 보관되는 정보를 사용하여 프로파일러는 어떤 함수가
호출되었는지, 몇 번이나 호출되었는 지를 알아내게 된다. 컴파일러의 `-pg'
옵션을 사용하면 컴파일러는 프로그램내의 모든 함수에 프로파일링용 내부 함수 -mcount
: 운영체제나 컴파일러의 버전에 따라 _mcount
, 또는 __mcount
로
이름이 바뀔 수도 있음 -에 대한 호출을 최초의 동작중 하나로 삽입한다.
프로파일링 라이브러리에 포함되어 있는 mcount
루틴은 메모리상의
call graph table에 자신의 parent routine, child routine과 parent's parent routin을
기록한다. 이러한 과정은 일반적으로 child의 주소와 original parent내에 있는 return
address를 찾기 위한 스택 프레임(stack frame) 검사를 통해 이루어진다. 그러나
이러한 과정에서 사용되는 연산들은 머쉰에 따라 다르기 때문에, 대부분 mcount
루틴 자체는 필요한 정보를 추출한 후, frompc,
selfpc
인자를 사용하여 일반 C함수인 __mcount_internal
함수를 호출하도록
코딩된 short assembly-language stub routine이다. mcount
루틴내부에서 호출되는
__mcount_internal
함수는 메모리상의 call graph를 관리한다. call
graph는
__mcount_internal
함수에서 인자로 받은 i) frompc
, selfpc
와
ii) call arcs 각각에 대한 traversal 횟수를 기록한다.
GCC version 2에서는 스택 프레임에서 프로파일링에 필요한 정보를 추출하는 범용 mcount
함수를
가능케하는 __builtin_return_address
함수가 제공된다. 그러나 일부
컴퓨터, 특히 SPARC 머쉰에서 __builtin_return_address
함수는 많은
computation을 필요로 하기 때문에 성능상의 이유로 어셈블리 버전의 mcount
가
사용된다.
라이브러리 루틴에 대한 호출 횟수 정보는 프로파일링용 C라이브러리(`-lc_p' 옵션에 의해 지정되는 libc_p.a)-에서 수집된다. 프로파일링용 C라이브러리는 `-pg' 옵션으로 컴파일한다는 점을 제외하고는 일반 C라이브러리와 동일하다. 프로그램 링크시에 `gcc ... -pg' 옵션을 사용하면 자동으로 프로파일링용 C라이브러리와 링크된다.
프로파일링에는 프로그램 수행을 감시하고, 프로그램 카운터(PC: program counter)가 발생하는 지점의 히스토그램을 유지하는 작업이 포함된다.. 프로그램 실행중에 통상 1초에 100회정도 프로그램 카운터를 검사한다. 그러나 프로그램 카운터의 정확한 검사 빈도는 시스템마다 달라질 수 있다.
프로그램 카운터의 검사는 다음에 설명할 두가지 방식중 하나를 사용한다. 대부분의
유닉스 계열 운영체제에서는 memory array를 scale factor와 함께 커널에 등록하는
profil()
시스템 콜이 제공된다( scale factor에 의해 프로그램의
주소 공간이 어떻게 memory array에 매핑될지 결정된다). 일반적인 scaling값은 주소공간의
2에서 8바이트마다 하나의 memory array slot에 맵핑한다. 프로파일링 기능이 추가된
프로그램의 실행시, 시스템 클럭 틱마다, 프로그램 카운터의 값을 검사하여 memory
array내의 대응하는 slot을 증가시킨다. 이러한 일련의 작업은 커널에서 수행되기 때문에,
약간의 시스템 오버헤드를 가지게 된다( 커널은 clock interrupt를 처리하기 위하여
프로세스를 인트럽트하는데 이 부분이 오버헤드로 작용한다).
그러나 일부 운영체제- 특히 Linux 2.0 또는 그 이전 버전 -에서는 profil()
시스템
콜이 제공되지 않는다. profil()
시스템 콜이 제공되지 않는 운영체제에서는
커널이 setitimer()
함수를 사용하여 주기적으로 프로세스에 프로파일링
시그널을 전달하는 방식을 사용한다. 프로파일링 시그널이 프로그램 카운터를 검사하고
memory array내에 있는 slot을 증가시킨다. 이 방법은 샘플링할 때마다 user space에
시그널을 전달하여야 하기 때문에, 커널 기반의 프로파일링보다 오버헤드가 많아지게
된다. 또한 시그널 전달에 따른 시간 지연으로 인해, 정확도가 다소 떨어지는 단점도
있다.
startup routine monstartup
은 먼저 히스토그램에 필요한 메모리를
할당하고, 위에서 설명한 profil()
함수를 호출하거나 또는 클럭 시그널
핸들러(clock signal handler)를 설정하는 방식중 하나를 사용하여 프로파일링을
위한 작업을 개시한다. monstartup
은 여러 가지 방식으로 호출될 수
있다. Linux 시스템의 경우, 프로파일링 기능을 활성화하여 컴파일하면 일반적으로
사용되는 startup file인 crt0.o
대신에 프로파일링을 위한 startup
file인 gcrt0.o
를 사용한다. gcrt0.o
는 먼저 monstartup
를
호출하고 다음에 main
을 호출한다. 프로파일링을 위한 startup file
gcrt0.o
가 사용되게 하려면 링크시에 `gcc ... -pg' 를
사용하여야 한다. SPARC 시스템의 경우에는 특별한 startup루틴이 사용되지는 않고,
대신에 대개는 main
이 호출되었을 때 처음으로 호출되는 루틴 mcount
함수에서
monstartup
을 호출한다.
컴파일러의 `-a' 옵션이 사용된 경우, 기본 블록 카운팅 기능도 활성화되고,
각각의 오브젝트 파일은 초기값을 0로 하는 카운트들의 정적 배열(static
array of counts)과 함께 컴파일된다. 실행코드 내부에는 새로운 기본 블록이 시작할
때 마다 (즉, if
문이 나타날 때마다), 그 위치에 배열내의 해당 카운트를
증가시키는 인스트럭션들이 삽입된다. 컴파일 시에 각각의 기본 블록의 시작
주소를 기록하기 위한 주소의 쌍을 원소로 하는 배열(paired array)이 만들어진다.
이들 자료구조들- 오브젝트화일에 기본 블록 카운팅을 위해 추가된 정적 배열과 컴파일시에
기본 블록의 주소를 기록하기 위해 만들어진 주소의 쌍을 원소로 하는 배열-을 통해
기본 블록이 실행된 횟수와 기본 블록의 주소를 기록하게 된다.
프로파일링 라이브러리에는 cleanup routine mcleanup
이 포함되어 있다.
cleanup routine mcleanup
은 대개 프로그램 종료시에 자동으로 호출되도록
atexit()
함수를 사용하여 등록된다. mcleanup
은 메모리상에
있는 프로파일링 관련 정보를 `gmon.out' 파일에 기록하는 일을 수행한다.
프로파일링이 다 끝나면 여러 가지 헤더들이 출력되고, 히스토그램이 기록되며, 마직막으로
call graph arc와 기본 블록 카운팅이 출력되게 된다.
gprof
에서 나온 출력을 통해서 프로그램내의 어떤 부분이 I/O또는 swapping
bandwidth로 인해 제한을 받고 있는지에 대한 정보를 알 수는 없다. 이는 프로그램
카운터의 생플들이 프로그램 실행중(In run state)에 고정된 간격으로 수집되었기
때문이다. 따라서 gprof
출력내에 있는 시간 측정값들에는 프로그램이
swapping이나 I/O또는 프로세스 스케쥴링에 의해 실행 상태에 있지 않을 때( not
in run )의 시간에 대한 정보는 하나도 포함되어 있지 않다. 예를 들어, 프로그램의
특정 부분에서 물리적 메모리보다 큰 양의 데이터를 만들어 낼 경우에는 thrashing으로
인해 상당히 느려 질 수 가 있다. 그러나 gprof
에서는 thrashing으로
인해 느려졌던 기간을 아주 짧은 시간으로 보고할 수도 있다. 한편, 실행 시간에
따른 샘플링은 시스템내의 다른 사용자들에 의해 발생한 로드에 의해 프로파일링
출력 결과가 직접적인 영향을 받지 않는다는 장점을 가지고 있다.
프로파일 데이터에 사용되었던 이전 BSD계열의 파일 포맷은 data file이
gprof
file인지를 검사하는데 사용하는 magic cookie
가
포함되어 있지 않다. 또한 이 포맷에는 버전 번호(version number)를 가지고 있지
않았기 때문에, 파일 포맷을 변경하기가 불가능하였다. GNU gprof
는 magic cookie
와 버전 번호(version number)를 모두 제공하는 새로운
파일 포맷을 사용한다. 이전 버전과의 호환성을 위해서 GNU gprof
가
계속해서 이전 BSD계열의 포맷을 지원하기는 하지만 모든 기능이 제공되는
것은 아니다. 예를 들어, 기본 블록 실행 카운트와 같은 기능은 이전 파일 포맷으로는
표현할 수 없다.
GNU gprof
에서 사용하는 새로운 파일 포맷은 `gmon_out.h'에 정의되어 있다. 새로운
파일 포맷은 추후 확장을 위한 예비 바이트들과 magic cookie와 버전 번호를 포함하고
있는 헤더로 구성되어 있다. 프로파일 데이터 파일내에 있는 모든 데이터는 프로파일
데이터가 수집된 호스트의 아키텍쳐를 따르는 포맷으로 되어있다. GNU gprof
은
해당 호스트의 아키텍쳐에서 사용중인 byte-order에 자동으로 맞춘다.
새로운 파일 포맷에서는 헤더, 레코드의 순으로 출력된다. 현재는 세 개의 서로
다른 레코드 타입- 히스토그램 레코드, call-graph arc 레코드, 기본 블록 실행 카운트
레코드- 이 있다. 각각의 프로파일링 데이터 파일은 임의 갯수개의 레코드 타일을
포함할 수 있다. GNU gprof
는 프로파일링 데이터 파일을 읽어들인후,동일한
타입의 레코드들끼리 서로 호환성이 있는지를 확인하고 모든 레코드들의 합집합을 구한다.
예를 들어, 기본 블록 카운트 레코드 타입의 경우, 이들 레코드의 합집합은 모든
기본 블록의 실행 카운트의 단순 합이다.
히스토그램 레코드는 헤더와 bins의 배열로 구성된다. 헤더에는 히스토그램이 채워지는 text-segment range, 바이트 단위의 히스토그램 크기(구형 BSD포맷과 달리, 이 크기에는 헤더의 크기는 포함되지 않는다), 프로파일링 클럭의 속도, 프로파일링 클럭 속도에 따라 스케일된 후에 bin 카운트들이 나타내는 물리적 단위(physical dimension)가 포함되어 있다. 물리적 단위는 최대 15개의 문자로 구성된 이름과 1개의 문자로된 약자로 표시된다. 예를 들어, 실시간(real-time)을 나타내는 히스토그램은 실제 수치 단위 이름으로 "seconds", 실제 수치 단위의 약자로 "s"로 표시한다. 이 기능은 성능 모니터 하드웨어(performance monitor hardware)를 지원하는 아키텍쳐에서 유용하게 사용될 수 있다. 예를 들어, DEC OSF/1에서는 "uprofile" 명령어를 사용하여 인스트럭션의 캐쉬 미쓰에 대한 히스토그램을 만들 수 있다. 이 경우, 히스토그램 헤더에 있는 실제 수치 단위의 이름은 "i-cache misses" 로, 단위의 약어는 "1"로 설정할 수 있다( 캐쉬 미쓰의 횟수는 단순 카운트이지 물리적인 단위가 아니기 때문이다). 또한 이 경우에 프로파일링 속도(profiling rate)는 1로 설정되어야 한다.
히스토그램 bin은 16비트 숫자로 각각의 bin은 동일한 크기의 text-space를 나타낸다. 예를 들어, text-segment가 1,000 바이트이고 히스토그램에 10개의 bin이 있다면, 각각의 bin은 100바이트를 나타낸다.
Call-graph 레코드는 BSD계열의 파일 포맷에서 사용한 것과 동일한 포맷을 가지고 있다. Call-graph 레코드는 call graph의 하나의 arc와 프로그램 실행중에 arc가 traversed된 횟수를 나타내는 카운트로 구성된다. Arc는 주소의 쌍으로 표현되며, 첫 번째 주소는 caller 함수의 내부에 있는 주소이고, 두 번째 주소는 callee함수내부에 있는 주소이어야 한다. 함수 단위로 프로파일링을 수행할때, 이들 주소는 각각의 함수 내부의 임의 위치를 포인트할 수 있지만, 라인 단위로 프로파일링할 때는 이들 주소가 호출이 발생한 지점(call-site)과 함수의 entry-point에 가능한 가까울수록 좋다. 라인 단위 프로파일링에서 호출지점또는 enrty-point에 가능한 근접한 주소를 arc에서 사용함으로서 라인 단위의 Call graph가 소스코드의 어느 라인에서 특정 함수가 호출되었는 지를 정확하게 구분하도록 보장한다.
기본 블록 실행 카운트 레코드는 헤더와 address/count 쌍들의 시퀀스로 구성되어 있다. 헤더는 시퀀스의 크기를 나타내며, 각각의 address/count쌍에서 address는 기본 블록과 연관된 주소이며, count는 기본 블록이 실행된 횟수이다. 기본 블록 내에 있는 임의 주소가 address에 사용될 수 있다.
gprof
의 내부 동작
과정
다른 모든 프로그램과 마찬가지로 gprof
는 먼저 명령행 옵션을
처리한다. Symspecs을 사용하는 옵션이 명령행에서 설정되어 있으면 명령행 옵션
처리를 하는 중에,
gprof
는 자신의 symspec 리스트
(sym_ids.c:sym_id_add
) 를 만든다.
gprof
는 symspecs를 단일 연결 리스트(single linked list)로 유지하고, 단일 연결 리스트는
나중에 12개의 심볼 테이블들로 변환된다. 12개의 심볼 테이블들은 6개의 include/exclude
쌍으로 구성된다. 여섯 개의 include/exclude쌍은 다음과 같다.
1) flat profile용 (INCL_FLAT/EXCL_FLAT),
2) call graph arcs용 (INCL_ARCS/EXCL_ARCS),
3) call graph내에서의 출력기능 제어용 (INCL_GRAPH/EXCL_GRAPH),
4) call
graph에서의 시간 전파 기능 제어용 (INCL_TIME/EXCL_TIME),
5) annotated source
리스팅 기능 제어용 (INCL_ANNO/EXCL_ANNO),
6) 실행 카운트(execution count)
리스팅 기능 제어용(INCL_EXEC/EXCL_EXEC).
명령행 옵션을 처리한 후에, gprof
는 다음과 같은 작업을 통해 symspec 리스트를
작성하는 작업을 마친다.
1)
default_excluded_list
에 있는 모든 symspecs를 exclude lists EXCL_TIME,
EXCL_GRAPH에 추가,
2) 라인 단위 프로파일링이 지정된 경우에는 default_excluded_list
에
있는 symspecs를 EXCL_FLAT 에도 추가
(EXCL_ANNO, EXCL_ARCS 와 EXCL_EXEC에는 default_excluded_list
에
있는 symspecs가 추가되지 않는다.)
다음으로, 적절한 크기의 심볼 배열을 malloc한 후에는 오브젝브 파일을 열고,
오브젝트 파일인지 검사하고, bfd_canonicalize_symtab
를 사용하여
오브젝트의 심볼 테이블(core.c:core_init)을 읽기 위하여 BFD라이브러리가 호출된다.
`--file-ordering' 옵션이 명령행에서 설정되었다면, 이 시점에서
함수 매핑이 읽혀지고, `-c' 옵션이 주어진 경우 core text
space가 메모리로 읽혀들여진다.
이때서야 비로소 Sym structure
의 배열인 gprof 의 자체 심볼 테이블이
만들어진다. 이 과정은 라인 단위 프로파일링을 지정하는 `-l' 옵션 설정여부에 따라,
다음에 설명할 두가지 방식중 하나를 사용하여 수행된다. 일반적인 프로파일링의
경우, BFD canonical symbol table을 검사한다. 라인 단위 프로파일링일 때에는
모든 text space address를 검사하여 라인 번호가 바뀔 때마다, 새로운 심볼 테이블
엔트리가 생성된다. 어느 방식이든, 필요한 심볼테이블 크기를 계산하는 과정과 실제
심볼을 읽어들이는 2단계로 구성된다. 이들 단계의 중간에서, 적절한 크기 Sym
타입의
단일 배열이 생성된다. 마지막으로 symtab.c:symtab_finalize
을 호출하여 심볼 테이블을 소팅하고, 중복된 심볼 엔트리(동일한 주소를 가진 심볼
엔트리)를 삭제한다.
심볼 테이블은 다음과 같은 두가지 이유로 연속적인 배열(contiguous array)이어야
한다. 첫째 심볼 테이블을 소트하기 위해 배열을 소트하는 qsort
라이브러리
함수를 사용하기 때문이다. 또한 메모리 주소에 기반하여 이진 탐색 알고리즘을 사용하는
심볼 검색 루틴(symtab.c:sym_lookup
)에서 심볼테이블이 sorted array이도록
요구하기 때문이다. 함수 심볼은 is_func
플래그로 구분되고, 라인
번호 심볼은 특별한 플래그 설정이 없다. 추가적으로 로컬 심볼일 경우에는 이를
나타내기 위하여 is_static
플래그를 가진다.
읽어들인 심볼테이블을 사용하여, symspecs을 Syms로 번역한다(sym_ids.c:sym_id_parse
).
하나의 symspec이 복수개의 심볼을 나타낼 수 있다는 사실을 기억하여야 한다. 심볼
테이블(syms
)의 배열이 한 개 생성된다. 심볼 테이블의 각각의 엔트리는
각각의 리스팅에서 포함 또는 배제될 syms
의 테이블이다. 마스터
심볼 테이블과 symspecs는 중첩된 루프에 의해 검사되고, symspecs에 일치하는 모든
심볼은 적절한 syms 테이블에 삽입된다. 이 과정은 두 번 수행되는데, 첫 번째 수행에서는
각각의 심볼 테이블에 필요한 크기를 계산하고, 두 번째 수행에서 실제로 테이블을 만든다.
테이블을 위한 공간은 첫 번째 수행과 두 번째 수행사이에서 malloc된다. 그리고나서
어떤 심볼이 include symspecs list또는 exclude symspec list에 있는 지를 결정하기
위해, gprof
은 syms
배열에 있는 적당한 테이블에 심볼
탐색 루틴을 사용한다.
다음으로 프로파일 데이터 파일 자체를 읽는데
(gmon_io.c:gmon_out_read
), 먼저 새로운 스타일의 gmon.out' 헤더를
검사하고, magic number를 검사하여 실패하면, 구형 BSD `gmon.out'
으로 가정한다.
hist.c:hist_read_rec
함수는 새로운 스타일의 히스토그램 레코드들을
읽는다. 최초 히스토그램 레코드에 대하여 모든 bin을 보관할 수 있는 메모리를 할당하고,
모든 bin을 읽어들인다. 복수개의 프로파일 데이터 파일 또는 복수개의 히스토그램
레코드를 가진 파일을 읽어들일 때, 시작 주소, 끝주소, bin의 갯수, 샘플링 속도가
여러 히스토그램들간에 일치하여야 한다. 그렇지 않은 경우 fatal error이 발생한다.
모든 히스토그램이 일치하는 경우, 추가적인 히스토그램들을 기존의 in-memory array에
더한다.
call graph 레코드가 읽혀지면서(call_graph.c:cg_read_rec
),
parent와 child addresses가 심볼 테이블 엔트리에 매치되며, arc가 INCL_ARCS/EXCL_ARCS를
사용한 symspec 검사에서 성공하는 경우, cg_arcs.c:arc_add
를 사용하여
call graph arc가 생성된다. 각각의 arc가 추가되면서, 연결 리스트(linked list)는
parent's child arc, child's parent arc를 정리한다. child의 call count와 arc의
call count는 레코드의 call count만큼 증가된다.
라인단위 프로파일링이 선택됐을 경우에만 기본 블록 레코드들이 읽혀진다(basic_blocks.c:bb_read_rec
).
각각의 기본 블록 address는 심볼 테이블의 상응하는 라인 심볼과 매치되고, 심볼의
bb_addr 와 bb_calls 배열에 엔트리가 만들어진다. 동일한 주소를 가지는 복수개의
기본 블록이 있다면, call count는 누적된다.
gmon.sum 파일 덤프를 위한 옵션이 지정된 경우에는 gmon.sum 파일을 덤프한다(gmon_io.c:gmon_out_write
).
데이터 파일 내에 히스토그램이 있다면, 모든 샘플 bin을 순회하여 반복하면서 히스토그램을
심볼에 할당한다(hist.c:hist_assign_samples
). 심볼 테이블은 메모리
주소의 오름차순으로 소트되어 있으므로, 샘플 bin에 대하여 이동하는 것은 단순히
심볼테이블을 순서대로 이동하는 것과 같다. 이 과정에 INCL_FLAT/EXCL_FLAT에 대한 symspec check가 포함되어
있다. 히스토그램 scale factor에 따라, 샘플 bin은 복수개의 symbols을 커버할 수
있으며, 이 경우 샘플 카운트의 fraction(오버랩되는 정도에 비례하는)이 각각의
심볼에 할당된다. 이러한 효과는 일반적인 프로파일링에서는 드문 경우지만, 라인단위
프로파일링에서 오버랩은 좀더 흔히 볼 수 있다. 예를 들어 두 개의 인접한 라인들이
1/2 hit를 만들어 낼 수도 있다.
데이터 파일 내에 call graph 데이터가 있는 경우, cg_arcs.c:cg_assemble
이 호출된다.
먼저 `-c' 옵션이 지정되었다면, 머쉰에 의존적인 루틴 (find_call
) 이
각각의 심볼의 머쉰 코드를 검사하여 subroutine call 인스트럭션을 찾아 0번의
호출 카운트(call count)를 가진 call graph에 추가한다. 모든 심볼들을 depth-first
numbering을 사용하여 topological sort하여(cg_dfn.c:cg_dfn
), children이
항상 그들의 parent보다 적은 번호를 가지도록 만든다. 그 다음에 심볼 테이블에
대한 포인터들을 원소로 하는 배열을 만들고, 이 배열의 모든 원소들에 동일한 topological
number를 부여한다. 소트된 심볼 포인터의 배열에 대하여 다음과 같은 2단계 작업을
수행한다.
1) 끝에서 시작쪽으로 (parents에서 children 방향으로) child
time의 fraction을 계산하고 각각의 parent와 print flag에 propagate한다. parent의 include/exclude (print/no print) 특성이
( children이 INCL_GRAPH 또는 EXCL_GRAPH 에 명시적으로 나타나있지 않은 경우에만)
children에 pripagate되면서, print flag는 INCL_GRAPH/EXCL_GRAPH에 대한 symspec
처리를 반영한다.
2) 시작에서 끝쪽으로(children에서 parents방향으로),
call grapht를 따라 INCL_TIME/EXCL_TIME 검사에 따라 timings을 실제로 propagate한다.
the print flag, fractions, 와 timings이 심볼 구조에 저장되면서, topological
sort array는 없어지고, propagated time에 따라 소트된 포인트의 배열이 새로 만들어
진다.
마지막으로, 사용자가 요청한 다양한 형태의 출력이 프린트된다. call graph (cg_print.c:cg_print
)와
flat profile (hist.c:hist_print
)은 이미 계산된 값을 프린트한 것이다.
annotated source 리스팅
(basic_blocks.c:print_annotated_source
)은 기본 블록 정보가 있는
경우 이를 사용하여 call count가 있는 각각의 코드 라인을 레이블링한다. 그렇치
않고 기본 블록 정보가 없는 경우에는 함수 호출 카운트(function call counts)만
출력한다.
function ordering과 관련된 코드는 소스 코드(cg_print.c)에 일부 잘 문서화되어 있다. function ordering은 기본적으로, 가장 많이 사용된 함수와 가장 최상위의 parent가 먼저 위차하고, 그 다음 순위의 함수, 적게 사용된 함수, 사용되지 않은 함수순으로 배치한다.
gprof
디버깅gprof
프로그램이 디버깅 활성화 설정으로 컴파일되었다면, `-d' 옵션은
gprof
의 내부 동작에 대한 이해를 도울 수 있는 디버깅 정보를 표준
출력에 프린트한다. '-d' 옵션의 옵션 인자로 명시된 디버깅 번호는 다음의
옵션의 합이다.