'C/C++/패턴 및 리팩토링'에 해당되는 글 2건
- 2010.07.07 파일간 #include 설계에 대한 생각
- 2010.06.25 단기간에 많은 처리를 하는 선행조건을 가지는 함수의 분리
프로그래밍을 반복할 때마다 어떻게해야 include를 효율적으로 나타낼수 있을까 수없이 고민을 해왔다. 먼저 필요한 헤더만 포함하는 것은 가독성에 상당히 좋으므로 참으로 현명한 방법이라 생각한다. 하지만 이는 때때로 치명적이다. c/c++에서는 include를 중복으로 해버리면 에러가 나버리고, 교차포함한 경우 짜증지수를 완전 올려주는 원인이 되기도 한다. 그 외로 include가 필요하지 않은 곳에 하거나 include 설계를 잘못한 경우 엄청난 컴파일시간에 직면할 수도 있기 때문이다. 그렇다면 최고의 방법은 무엇일까.. 아직도 잘 모르겠다.
1. 아무것도 포함하지 않는 헤더는 필요하다면 일단 무조건 포함시킨다.
'아무것도' 라는말이 좀 걸리는데 사실 c/c++ 표준라이브러리는 포함되어도 아무것도 포함되지 않은 헤더로 간주한다. c/c++ 표준 라이브러리에서 우리의 라이브러리를 포함할 이유가 없기 때문에 교차포함의 두려움이 없다. 따라서 이건 명백하게 포함시킬 수 있는 부분이다. 헤더가 아무것도 포함하지 않았으므로 교차포함의 두려움은 없다. 흔히 이런 헤더는 어떠한 자료구조를 모아놓은 경우가 많다. 버텍스를 정의하는 구조체.. 어떤 기능을 정의하는 구조체. 만약 그 헤더에 우리 라이브러리중 하나의 헤더를 포함하고 있다면 조심할 필요가 있다.
2. 헤더에는 짧은 시간에 자주 실행되는 인라인 함수만 포함시킨다.
이것은 당연한 얘기로.. 짧은 시간에 자주 실행되는 함수라면 최적화를 위해 인라인 함수(물론 아주 짧은 함수의 경우)로 만들어야 한다. 그것을 제외하고는 소스파일에 포함시키는 방법이다. 아래는 상속을 받은 경우 포함할 필요가 없다는 것을 보여준다. 어차피 컴파일은 소스파일로 하는 것이니까. 단, 포함 순서가 중요하다
----------- Parent.h --------------
class CParent
{
};
----------- Child.h --------------
class CChild : public CParent
{
}
----------- Child.cpp --------------
#include "Parent.h"
#include "Child.h"
...
class CParent
{
};
----------- Child.h --------------
class CChild : public CParent
{
}
----------- Child.cpp --------------
#include "Parent.h"
#include "Child.h"
...
3. 부득이 교차포함해야 하는 경우는 부모급 헤더에서만 포함시킨다.
관리하기 아주 쉬운 형태로 부모급헤더에만 포함시키고, 자식금 헤더에서는 class CTest; 라는 식으로 암시만 주고, 소스파일에서 부모급헤더를 포함시키는 방법이다. 프로그래밍하다보면 자식급의 부류가 상당히 많아질 수 있는데.. 그럴때 관리하기 아주 편한 형태라 생각한다. 하지만 이런 구조의 경우 포함되는 부모급클래스를 인라인함수로 호출할 수 없다. 그러나 자식급 클래스는 대부분 부모급 클래스와 생명줄이 연결되어 있긴 하지만 부모클래스에 어떤 처리를 부탁하는 일이 없기 때문에 인라인 함수를 만들라고 하더라도 부모급 클래스의 레퍼런스만 리턴해주는 경우가 대부분이기 때문에 문제가 되는 경우는 드물다. 하지만 부모급 헤더에서는 인라인 함수가 자주 나올 확률이 높다. 그렇기 때문에 부모급 헤더에만 포함시키는 것이 좋다.
이 경우는 장점과 단점을 모두 지니는데, 그렇게 하다보면 부모급 클래스에선 다량의 헤더파일이 추가될 것이고 그것을 필요로 하는 모든 소스파일은 모두 다시 컴파일 될것이다.(물론 헤더가 수정되었을 때 이야기다) 하지만 포함시킬때는 부모클래스헤더만 포함시키면 그 예하에 포함되어있는 모든 헤더가 포함된셈이므로 포함할 필요가 없긴하다. 하지만 이 경우 가독성이 떨어지는 문제가 생기기도 한다. 아래를 보자.
----------- Mgr.h --------------
#include "A.h"
#include "B.h"
#include "C.h"
class CMgr
{
A* a;
B* b;
C* c;
};
----------- A.h --------------
class CMgr;
class CB;
class CA
{
CMgr* mgr;
CB* b;
};
----------- A.cpp --------------
#include "A.h"
#include "Mgr.h"
CA::CA()
{
mgr ..
b ..
}
#include "A.h"
#include "B.h"
#include "C.h"
class CMgr
{
A* a;
B* b;
C* c;
};
----------- A.h --------------
class CMgr;
class CB;
class CA
{
CMgr* mgr;
CB* b;
};
----------- A.cpp --------------
#include "A.h"
#include "Mgr.h"
CA::CA()
{
mgr ..
b ..
}
단순히 저렇게 처리할 수도 있다는 얘기다. 심지어 A.cpp의 A.h를 없애도 상관은 없다. (지금 상태로는) 가독성이 떨어진다고 말했었는데 A.cpp에 B.h가 include되어있다면 A.cpp에서 B.h를 쓰고있구나.. 라는 정보를 얻을 수 있다. 하지만 저 상태로는 B.h를 쓰고 있구나 하는 정보를 당장엔 알 수 없다. 또한 여기서 B.h헤더를 고쳐주게 되면 Mgr.cpp가 재컴파일되고 A.cpp도 재컴파일된다. 그 이유는 B.h가 Mgr.h에 포함되어있고 Mgr.g는 A.cpp에 포함되어 있기 때문이다. 이것은 프로젝트가 거대하다면 단 한글자만 고쳐도 엄청난 컴파일시간이 도사릴 수 있다는 것을 알려준다. 따라서 Mgr.h만 포함하면 모두 다 될거란 생각은 버려야한다. 컴파일 시간이 두렵다면.. 하지만 불행중 다행으로.. B.h가 고쳐진 경우가 아닌, B.cpp가 고쳐진경우는 링크만 다시 되는 형태로 된다. 왜냐하면 헤더는 변하지 않았으니까. 따라서 인라인함수가 많거나 템플릿은 저렇게 포함될 때 고민좀 해봐야한다. 또한 #pragma once 등으로 A.cpp에 Mgr.h와 B.h를 둘다 포함시킬 수 있다. 그러면 가독성은 일단 해결이다.
4. 1, 2번만을 활용하고 소스파일엔 필요한 것만 포함시킨다.
어떻게 보면 3번은 컴파일시간을 대량으로 늘리는 계기가 될수도 있다고 생각한다. 따라서 3번도 2번으로 최적화 시킨 후, 소스파일에는 일일이 필요한 것만 포함시킨다. 이것은 간혹 너무도 귀찮지만(실제로 너무 귀찮다.), 무슨 헤더가 포함되어있는지 알기 쉽고 컴파일시간을 최소화시켜주기 때문이다.
5. 비슷한 부류는 하나의 헤더로 묶은 후 그 헤더를 포함시킨다.
이것도 당연한 얘기다. 일단 여러 헤더를 하나로 묶으면 더 간결해지고 알아보기 쉬운 형태로 바뀌게 된다. 특히 이런 것들은 미리 컴파일된 헤더(또는 라이브러리)로 만들어놓고 묶어서 헤더를 포함시키는 경우가 많다. 예를 들어 특정 외부 라이브러리를 대표하는 헤더파일들이 그 예라고 볼 수 있겠다. (ex) ogre.h, d3d9.h)
'C/C++ > 패턴 및 리팩토링' 카테고리의 다른 글
단기간에 많은 처리를 하는 선행조건을 가지는 함수의 분리 (0) | 2010.06.25 |
---|
선행학습으로 인라인함수(Efficient C++)과 80-20의 법칙(Efficient C++, More Effective C++, 기타)을 읽어보시면 이해가 빠르십니다.
어떤 함수가 처리되는데 특정한 조건이 필요한 경우를 보자. 그런데 그 함수가 단시간에 자주 호출된다면, 명령어 하나라도 최적화할 필요가 있다.
파란색 - CQuadTreeNode의 소스
연두색 - CQuadTree의 소스
예를 들어 쿼트트리내 오브젝트들을 가져오는 경우 해당 노드에 다음과 같은 함수를 호출할 수 있다.
VOID CQuadTreeNode::CheckInFrustumAndNotifyToNode( CFrustum* pFrustum );
위 함수는 노드내 오브젝트와 절두체의 충돌판정 후 절두체와 충돌한다면 해당 오브젝트의 렌더링 콜백함수를 호출하는 함수로써 다음과 같은 형식을 가지고 있다. 하지만 선행조건이 걸려있다. m_iObjectCount <= 0, 오브젝트의 개수가 0보다 같거나 작다면 더이상 진행하지 않고 함수를 끝낸다.
CQuadTreeNode
VOID CQuadTreeNode::CheckInFrustumAndNotifyToNode( CFrustum* pFrustum )
{
if( m_iObjectCount <= 0 )
{
return;
}
for( SCENENODELIST::iterator it = m_sceneNodeList.begin() ;
it != m_sceneNodeList.end() ;
it++ )
{
if( pFrustum->IsAABBInFrustum(*(*it)->GetWorldBoundAABB()) )
(*it)->AddedRenderEntryArrayCallBack();
}
}
{
if( m_iObjectCount <= 0 )
{
return;
}
for( SCENENODELIST::iterator it = m_sceneNodeList.begin() ;
it != m_sceneNodeList.end() ;
it++ )
{
if( pFrustum->IsAABBInFrustum(*(*it)->GetWorldBoundAABB()) )
(*it)->AddedRenderEntryArrayCallBack();
}
}
이 함수는 쿼드트리클래스의 CQuadTree::CheckInFrustumAndNotifyToNode함수에서 다음과 같은 형식으로 호출될 수 있다.
CQuadTree
VOID CQuadTree::CheckInFrustumAndNotifyToNode( CFrustum* pFrustum )
{
// 쿼드트리의 영역 계산
...
for 쿼드트리의 영역 순회
{
CQuadTreeNode* node = GetNode( .. );
node->CheckInFrustumAndNotifyToNode( pFrustum );
}
}
{
// 쿼드트리의 영역 계산
...
for 쿼드트리의 영역 순회
{
CQuadTreeNode* node = GetNode( .. );
node->CheckInFrustumAndNotifyToNode( pFrustum );
}
}
렌더링 쿼드트리의 특성상 아주 작은 시간에도 매우 자주 호출됨에 따라 단 하나의 명령어도 간과해서는 안된다. 즉, CQuadTreeNode::CheckInFrustumAndNotifyToNode 함수의 함수호출 명령어조차도 최소화 시켜야하는데 이 얘기는 inline으로 선언해야한단 얘기다. 하지만 inline으로 하기에는 소스의 길이가 다소 길다.(싱글톤메소드라고 해서 예외사항은 있다.) 만약 위 소스는 절대 inline이 될 수 없다 가정하면,
오브젝트의 개수 부분만을 인라인화 해서 아예 이 함수가 호출되지 않게끔 방지할 순 있다. 다음을 보자. 수정된 부분은 굵은글씨로 표시해두었다.
CQuadTreeNode
inline BOOL CQuadTreeNode::IsExistNode()
{
return m_iObjectCount>0 ? TRUE:FALSE;
}
VOID CQuadTreeNode::CheckInFrustumAndNotifyToNode( CFrustum* pFrustum )
{
// if검사부분을 아예 없애버렸다.
// 이 경우는 다소 위험한 방법이긴한데 릴리즈시 약간의 오버헤드를 없앨 수 있다.
// 만약, CQuadTree::CheckInFrustumAndNotifyToNode함수에서만 호출된다면,
// 아래 코드는 99%안전한 코드다.
// 특정 위치의 한곳에서만 호출되는 것을 싱글톤 메소드라고 하는데 이 경우는 FORCEINLINE 매크로로
// 강제 인라인화 할 수 있다.(VS에서는 지나치게 긴 메소드를 인라인화에서 제외시킨다.)
assert( m_iObjectCount > 0 && "오브젝트가 하나도 없는데 호출됐네요." );
for( SCENENODELIST::iterator it = m_sceneNodeList.begin() ;
it != m_sceneNodeList.end() ;
it++ )
{
if( (*it)->IsInFrustum( pFrustum ) )
(*it)->AddedRenderEntryArrayCallBack();
}
{
return m_iObjectCount>0 ? TRUE:FALSE;
}
VOID CQuadTreeNode::CheckInFrustumAndNotifyToNode( CFrustum* pFrustum )
{
// if검사부분을 아예 없애버렸다.
// 이 경우는 다소 위험한 방법이긴한데 릴리즈시 약간의 오버헤드를 없앨 수 있다.
// 만약, CQuadTree::CheckInFrustumAndNotifyToNode함수에서만 호출된다면,
// 아래 코드는 99%안전한 코드다.
// 특정 위치의 한곳에서만 호출되는 것을 싱글톤 메소드라고 하는데 이 경우는 FORCEINLINE 매크로로
// 강제 인라인화 할 수 있다.(VS에서는 지나치게 긴 메소드를 인라인화에서 제외시킨다.)
assert( m_iObjectCount > 0 && "오브젝트가 하나도 없는데 호출됐네요." );
for( SCENENODELIST::iterator it = m_sceneNodeList.begin() ;
it != m_sceneNodeList.end() ;
it++ )
{
if( (*it)->IsInFrustum( pFrustum ) )
(*it)->AddedRenderEntryArrayCallBack();
}
그리고 쿼드트리에서도 약간의 수정이 필요하다. 수정된 부분은 굵은글씨로 표시해두었다.
CQuadTree
VOID CQuadTree::CheckInFrustumAndNotifyToNode( CFrustum* pFrustum )
{
// 쿼드트리의 영역 계산
...
for 쿼드트리의 영역 순회
{
CQuadTreeNode* node = GetNode( .. );
if( node->IsExistNode() )
node->CheckInFrustumAndNotifyToNode( pFrustum );
}
}
{
// 쿼드트리의 영역 계산
...
for 쿼드트리의 영역 순회
{
CQuadTreeNode* node = GetNode( .. );
if( node->IsExistNode() )
node->CheckInFrustumAndNotifyToNode( pFrustum );
}
}
성능이 비약적으로 상승한다.
'C/C++ > 패턴 및 리팩토링' 카테고리의 다른 글
파일간 #include 설계에 대한 생각 (0) | 2010.07.07 |
---|