헤더 파일은 적당히 나눠야 한다

Posted at 2008. 7. 24. 18:59 // in S/W개발 // by 김윤수


참 오랜만에 글을 올려 보네요. ^^; 이번 글도 오래전부터 생각은 하고 있었는데, 이제서야 맘을 먹고 쓰게 됐습니다. 언제나 이 게으름을 극복할 수 있을까요 ?

이번 글에서는 헤더 파일과 컴파일 시간간 관계에 대해서 얘기하려고 합니다. 실제 실험을 통해 설명하는 게 쉬울 것 같습니다.

C++에는 Standard Template Library 라는 게 있다는 건 모두 아시리라 생각합니다. STL의 헤더 파일을 보면 <vector>, <list>, <map>, <deque>, <stack> 등 각 필요한 기능별로 헤더 파일이 비교적 상세하게 나눠져 있는 걸 볼 수 있습니다. 만약 STL의 개발자가 이렇게 상세하게 나눠놓지 않고 <stl>이라는 하나의 헤더 파일만 include 하면 되도록 개발해 놓았다면 컴파일 시간에 어떤 영향을 미치게 될까요 ? 실험을 통해 어떤 영향이 있을지 살펴 보겠습니다.

다음 두 개의 소스를 비교해 보시기 바랍니다.

/// @file main.cpp
/// 지금처럼 STL 헤더 파일이 비교적 상세하게 나눠져 있는 경우
#include <vector>   /// 필요한 헤더 파일만 include 합니다.

using namespace std;

int main(void)
{
  vector<int> vi;

  return 0;
}

/// @file main_all.cpp
/// STL header 파일이 stl 하나로 합쳐져 있는 경우
#include <stl>    /// 전체 라이브러리 헤더 파일을 include 합니다.

using namespace std;

int main(void)
{
  vector<int> vi;

  return 0;
}

/// @file stl
/// STL 헤더 파일이 stl 하나로 합쳐진 경우를 흉내냄
#include <vector>
#include <list>
#include <map>
#include <set>
#include <deque>
#include <stack>
#include <functional>
#include <algorithm>

이 둘 간의 100번씩 연속해서 컴파일한 후 시간을 비교해 봤더니 다음과 같은 결과가 나오더군요.

main

real    0m23.919s
user    0m19.309s
sys     0m2.488s

main_all

real    0m32.496s
user    0m27.194s
sys     0m2.900s

참고로 제가 테스트한 환경은 다음과 같습니다.

Ubuntu 8.04
g++ 4.2.3
$ cat /proc/cpuinfo
processor    : 0
vendor_id    : GenuineIntel
cpu family   : 6
model        : 13
model name   : Intel(R) Pentium(R) M processor 2.00GHz
stepping     : 8
cpu MHz      : 798.000
cache size   : 2048 KB

위 결과에서 두 경우가 24:32로 상당한 컴파일 시간 차이가 있음을 알 수 있습니다. 헤더 파일을 하나로 합쳤을 때가 그렇지 않을 경우에 비해 거의 25% 정도 늘어나는 것을 확인할 수 있습니다.

제가 이전에 사내 어떤 공용 라이브러리 소스에서 모든 헤더 파일을 하나의 헤더 파일에서 include 해 놓고, 각 응용 프로그램은 그 헤더 파일만 include 하도록 해 놓은 것을 본 적이 있습니다. 이렇게 할 경우 위 실험 결과를 생각해 보건데, 전체 응용 프로그램을 컴파일하는데 얼마나 시간이 많이 걸릴지 예상할 수 있습니다.

많은 개발자들이 컴파일 시간에 대해서는 별로 신경쓰지 않지만, 프로젝트 규모가 커지다 보면 컴파일 시간이 기다리기 힘들 정도로 길어지는 경우가 많습니다. 그렇게 될 경우, 코딩-컴파일-디버깅 싸이클이 길어지게 되고, 전체 개발 일정에도 영향을 미칠 수 있습니다.

요즘은 빌드 기술도 많이 발전하여 distributed build니 parallel build니 컴파일 시간을 많이 단축시켜 주는 툴이 있기도 합니다. 프로젝트 규모가 워낙 크고 어느 정도 개발이 진행되었다면 이런 툴들을 활용하는 것이 당연하겠지만, 그보다는 먼저 개발하기 전에 소스 파일들 간의 dependency 가 너무 많이 걸려 있지는 않도록 주의깊게 설계하는 것도 중요합니다. 그런 dependency 를 줄일 수 있는 방법 중의 하나가 이 글에서 제안하고 있는 헤더 파일을 적당히 나누라는 규칙이 되겠습니다. 다음 경우를 생각해 보시기 바랍니다.

all.h가 a.h, b.h ~ z.h 까지 포함하고 있고, a.c 라는 소스는 b.h에 있는 함수를 사용하지만, all.h만을 include하도록 해 놓았다면 어떻게 될까요 ? a.c 의 dependency list 에는 all.h 뿐 아니라 a.h ~ z.h 도 모두 포함됩니다. 그렇다면, 그 중에 하나만 수정되더라도 a.c 는 재컴파일 될 것입니다. 더불어 all.h 를 include 했던 모든 소스 파일들이 컴파일 되겠지요.

물론 헤더 파일을 아주 상세하게 나눈다면, 심지어 함수 하나에 헤더 파일 하나 정도로 나눌 수도 있겠지요. 그렇지만 이렇게 할 경우, 너무 많은 헤더 파일들을 열어야 하니, 역시 개발자들이 불편하게 될 것입니다. 너무 많은 헤더 파일을 열게 되니, 역시 컴파일 시간에 좋지 않은 영향을 미칠 수도 있을 것이구요. 그렇다면 적당히 중용을 찾아야 할 것입니다.

프로젝트 규모가 커질 수록

헤더 파일은 적당히 나눠야 한다

라는 규칙을 명심하시기 바랍니다. 적당을 위한 기준은 개발편의성과 컴파일시간간의 tradeoff 임도 기억하시기 바랍니다.

참고로 실험을 위해 다음과 같이 Makefile 을 작성했습니다.

CXX = g++
CFLAGS = -Wall -c -I. -o
LDFLAGS = -lstdc++
SRCS = main.cpp main_all.cpp

main: main.o

main_all: main_all.o

%.o:%.cpp
        $(CXX) $(CFLAGS) $@ $<

depend:
        makedepend -- $(CFLAGS) -- $(SRCS)

clean:
        rm -f *.o main main_all core* *.bak

make 를 호출하기 위한 shell script 를 다음과 같이 작성하였고,

$ cat incltst
#!/bin/bash

usage()
{
  echo "Usage: incltst main|main_all"
}

if [[ $1 != "main" && $1 != "main_all" ]]
then
  usage
  exit 0
fi

for (( i=0 ; $i < 100 ; ++i )) ; do
  make $1
  make clean
done

실행 시간은 다음 명령을 수행하여 측정하였습니다.

$ time ./incltst main
$ time ./incltst main_all

다른 분들도 측정하실 수 있도록 소스 파일 첨부합니다.


여러분이 실험한 결과도 한 번 댓글로 알려 주시면 감사하겠습니다.

글 읽어 주셔서 감사합니다. 이 글의 출처를 다음과 같은 형식으로 알려주신다면 비상업적 용도로 얼마든지 퍼다 나르셔도 좋습니다. ^^;

원문출처: 헤더 파일은 적당히 나눠야 한다(김윤수의 이상계를 꿈꾸며)

제 글이 유익하셨다면 오른쪽 버튼을 눌러 제 블로그를 구독하세요. ->
블로그를 구독하는 방법을 잘 모르시는 분은 2. RSS 활용을 클릭하세요.
RSS에 대해 잘 모르시는 분은 1. RSS란 무엇인가를 클릭하세요.