페이지

2015년 1월 4일 일요일

매개변수에 독립적인 코드는 템플릿으로부터 분리시키자.

 아무 생각없이 템플릿을 사용하면 템플릿의 적, 코드 비대화(code bloat)가 초래될 수 있다. 똑같은 내용의 코드가 여러 벌로 중복되어 파일로 구워진다는 뜻이다. Code 자체만 보면 깔끔하지만, 이진 코드가 템플릿으로 인해 불어터지는 불상사가 일어난다는 얘기다.

 공통성 및 가변성 분석(commonality and variabiity analysis) 의 관점에서 살펴봐야 한다.

 어떤 함수를 만들고 있다가 무심코 다른 함수를 봤는데, Code의 일부가 비슷하면 어떻게 하나 ? Copy & Paste ? 당연히 이렇게 하다면 비오는 날 먼지날때까지 맞아야겠고... 공통 Code를 별도의 함수로 만들고 이 함수를 기존의 두 함수에서 호출하도록 수정하는게 고치야지. 당연히 이렇게 다들 할테고...
 클래스를 만드는데 다른 클레스에서 공통된 부분을 발견한다면 ? 당연히 공통 부분을 새로운 클래스에 옮긴 뒤, 원래 2개의 클래스에서 이 클래스를 상속받거나 객체 합성을 사용하도록 고쳐야한다.
 템플릿을 작성할때도 똑같은 분석을 하고 똑같은 방법으로 Code 중복을 막으면 된다. 하지만 우리의 뒷통수를 노리는 뜻밖의 전개가 하나 있다. 템플릿이 아닌 Code는 코드 중복이 명시적이다. 즉 눈에 보인다. 하지만 템플릿의 경우는 암시적이다. 피나는 수련을 통하여 감각적으로 알아채야 한다는 것이다.

template<typename T, std::size_t n>
class SquareMatric {
public:
  void invert();
};

SquareMatrix<double, 5> m1;
m1.invert();

SquareMatrix<double, 10> m2;
m2.invert();

 위의 Code를 보면 m1과 m2의 invert()가 각각 다른 함수를 호출한다는 것을 다들 알 것이다. 당연 같은 함수일 수는 없다. 하지만 행과 열의 크기를 나타내는 상수만 빼면 두 함수는 완전히 똑같다. 코드 비대화를 일으키는 일반적 형태 중 하나이다. 일반 함수일 경우는 어떻게 하나 ? 크기를 매개변수로 받는 함수를 하나 만들어서 나머지 2개의 함수에서 그것을 호출하는 식으로 만들면 될 것이다. 템플릿도 같은 방법으로 접근이 가능하다.

template<typename T>
class SMBase {
protected:
  void invert(std::size_t n);
};

template<typename T, std::size_t n>
class SquareMatric : private SMBase {
public:
  using SMBase<T>::invert;
  void invert(){ invert(n); } // 암시적 inline 선언
};

 모든 double형의 템플릿들은 크기가 달라도 같은 invert() 함수를 공유하게 된다. 호출에 드는 비용도 추가되지 않았다. invert() 함수가 inline 함수이기 때문이다. 하지만 아직 해결되진 않았다. SMBase의 invert()함수가 어떻게 데이터가 저장된 메모리에 접근을 할 수가 있을까 ? 가장 간단한 방법은 invert() 함수에 메모리 주소를 매개변수로 넣어주는 것인데, 만약 함수가 invert() 한개가 아니라 한 30개 된다면 다 넣을 것인가 ? 이건 좀 아니다. 다른 방법으로 메모리의 포인터를 SMBase가 저장하는 방법이 있다. 어차피 저장하는 김에 크기도 같이 저장하자. 그럼 원래 있던 invert()이 매개변수도 삭제가 가능하다.

template<typename T>
class SMBase {
protected:
  SMBase(std::size_t n, T* pm) : size(n), pData(pm) {}
  void invert();
private:
  std::size_t size;
  T* pData;
};

template<typename T, std::size_t n>
class SquareMatric : private SMBase {
public:
  using SMBase<T>::invert;
  SquareMatrix() : SMBase<T>(n, data) {}
  void invert(){ invert(); }
private:
  T data[n*n];
};

이렇게 만들면 동적 메모리 할당은 안해도 되지만 파생 클래스의 크기가 커진다. 이 방법이 마음에 안들면 데이터를 힙으로 옮기는 방법도 있다.

template<typename T, std::size_t n>
class SquareMatric : private SMBase {
public:
  SquareMatrix() : SMBase<T>(n, NULL), pData(new T[n*n]) {
     this->setDataPtr(pData.get());
  }
  void invert(){ this->invert(); }
private:
  boost::scoped_array<T> pData;
};

 대부분의 파생 클래스의 함수들은 inline 함수가 되어 기본 클래스 버전의 사본을 하나만 공유하면서도 호출 비용의 증가없이 사용이 가능하다.

 또 다른 방법으로는 파생 클래스의 포인터를 Base 템플릿이 가지고 있는 방법도 있다.

 이렇게 했을 경우 단순히 실행 코드 크기가 작아지는 것으로 끝나는 것이 아니다. 프로그램의 작업 세트 크기가 줄어들어 명령어 캐시 내의 참조 지역성도 향상된다. 얼핏보면 포인터 크기 하나만큼의 낭비가 일어나긴 하겠지만, 저걸 없에기 위해서 이런 저런 방법을 찾다간 객체지향적인 특징의 많은 것들을 포기해야 할 수도 있다.

 몇몇 C++ Compiler는 자체적으로 최적화 작업을 해주기도 한다. vector<int> 와 vector<long> 을 합쳐준다던지, list<int *>, ist<const int *>, list<SquareMatrix<long, 3>*> 등을 모두 list<void *>로 동작하게 한다든지 등등...

 * 템플릿을 사용하면 비슷비슷한 클래스와 함수가 여러 벌 만들어집니다. 따라서 템플릿 매개변수에 종속되지 않은 템플릿 코드는 비대화이 원인이 됩니다.

 * 비타입 템플릿 매개변수로 생기는 코드 비대화의 경우, 템플릿 매개변수를 함수 매개변수 혹은 클래스 데이터 맴버로 대체함으로써 비해화를 종종 없앨 수 있습니다.




 * 타입 매개변수로 생기는 코드 비대화의 경우, 동일한 이진 표현구조를 가지고 인스턴스화되는 타입들이 한 가지 함수 구현을 공유하게 만듦으로써 비대화를 감소시킬 수 있습니다.

댓글 없음:

댓글 쓰기