스토리텔링 개발자

[Effective C++] 44. 템플릿 코드 비대화 회피하기 본문

개발/Effective C++

[Effective C++] 44. 템플릿 코드 비대화 회피하기

김디트 2024. 7. 16. 11:27
728x90

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

 

 

 

템플릿의 장단점
  • 장점
    • 코딩 시간 절약
    • 코드 중복 회피
  • 단점
    • 코드 비대화
      • 거의 똑같은 내용의 코드와 데이터가 중복되어 이진파일로 구워진다.

 

 

 

템플릿 코드 비대화 회피 방법
  • 공통성 및 가변성 분석(commonality and variability analysis)
    • 공통 부분을 별도의 클래스로 옮긴 후 클래스 상속 혹은 객체 합성으로 공유한다.
  • 이미 이런 분석을 항상 사용해 왔을 것이며, 템플릿에도 똑같이 적용하여 코드 중복을 막으면 된다.
  • 하지만... 템플릿은 코드 중복이 암시적이라 감각적으로 알아차리는 수밖에 없다.

 

 

 

템플릿 코드 비대화 해결법을 코드로 알아보자
template<typename T, std::size_t n>
class SquareMatrix // 정방행렬
{
public:
    ...
    void invert(); // 역행렬로 만들기
};

// 사본을 두 개 만든다.
// invert 코드가 거의 동일함에도 중복 생성된다!!
SquareMatrix<double, 5> sm1; // 5x5 행렬
sm1.invert();
SquareMatrix<double, 10> sm2; // 10x10 행렬
sm2.invert();
  • 코드 중복을 제거한 버전
template<typename T>
class SquareMatrixBase
{
protected:
    void invert(std::size_t matrixsize);
    ...
};

template<typename T, std::size_t n>
class SquareMatrix : private SquareMatrixBase<T>
{
private:
    using SquareMatrixBase<T>::invert; // invert 가려짐 문제 해결(항목 33 참조)
private:
    void invert() { this->invert(n); } // 중복 코드 제거, 암시적 인라인
};
  • 수정된 사항
    • 모든 정방행렬은 오직 한 가지 SquareMatrixBase 클래스를 공유하게 되므로 코드 중복이 제거된다.
    • 함수 호출에 드는 추가 비용을 없애기 위해 암시적 인라인 함수로 구현했다.
    • 기본 클래스가 파생 클래스의 구현을 돕기 위함이라는 걸 명확히 하기 위해 private 상속을 사용했다.
  • 하지만 이 방식으로 구현하려면 문제가 있다.
    • 역행렬을 만들기 위해서는 실제 행렬의 데이터가 필요하나, 부모 클래스의 invert 함수에서 실제 행렬 데이터에 접근할 방법이 없다.
  • 부모 클래스로 실제 행렬 데이터 넘기기
    1. 접근해야 하는 데이터를 부모 클래스 함수의 매개변수로 넘겨준다.
      • 하지만 invert처럼 행렬 크기에 상관없는 동작방식의 함수가 매우 많다면?
      • 이들 모두에 매개변수를 추가하는 건 번거롭고 SquareMatrixBase에 같은 정보를 계속 넘겨주는 꼴이다.
    2. 접근해야 하는 데이터의 포인터를 SquareMatrixBase에 저장하게 한다.
      • template<typename T>
        class SquareMatrixBase
        {
        protected:
            SquareMatrixBase(std::size_t n, T* pMem) : size(n), pData(pMem) {}
            
            void setDataPtr(T* ptr) { pData = ptr; }
            ...
        private:
            std::size_t size; // 행렬 크기
            T* pData; // 행렬 값에 대한 포인터
        };
        
        template<typename T, std::size_t n>
        class SqaureMatrix : private SquareMatrixBase<T>
        {
        public:
            SquareMatrix() : SquareMatrixBase<T>(n, data) {}
            ...
        private:
            T data[n*n];
        };
      • 행렬 크기도 저장하지 않을 이유가 없기 때문에 함께 저장했다.
      • 이렇게 하여 T가 동일하며, 행렬 사이즈만 다른 경우에는 동일한 부모 클래스를 참조하게 된다.

 

 

 

코드 중복을 제거한 버전의 장단점
  • 단점
    • 코드 중복을 제거하기 전의 버전이 이후의 버전보다 좋은 코드를 생성할 가능성이 높다.
      • 코드 중복 전의 버전에서는, 행렬 크기가 컴파일 시점에 투입되는 상수이기 때문에 상수 전파(constant propagation) 등의 최적화가 먹혀들 여지가 있다.
      • 생성되는 기계 명령어에 이 크기 상수 값이 즉치 피연산자(immeditate operand)로 바로 박힐 수 있다.
    • 아무 생각 없이 기본 클래스 쪽으로 코드를 옮기다 보면 각 객체의 전체 크기가 늘어나게 된다.
      • SquareMatrixBase 클래스에 들어가는 포인터는, 파생 클래스의 데이터의 중복이라 볼 수 있다.
      • 즉, SquareMatrix 객체 하나의 크기는 최소한 포인터 하나 크기만큼 낭비된 것이라 할 수 있다.
  • 장점
    • 실행 코드의 크기가 작아진다.
      • 이로 인해 프로그램의 작업 세트(working set, 한 프로세스에서 자주 참조하는 페이지의 집합) 크기가 줄어든다.
      • 명령어 캐시 내의 참조 지역성(locality of reference, 프로세스의 메모리 참조가 실행 중에 균일하게 흩어져 있지 않으며 특정 시점 및 특정 부분에 집중된다는 경험적/실험적 특성)도 향상된다.
      • 따라서 프로그램의 실행 속도가 빨라질 수 있다.
  • 어떤 효과가 우선일까?
    • 정확한 판단을 위해서는 사용 중인 플랫폼 및 대표 데이터 집합에 대해 두 방법을 모두 적용해 보고 결과를 관찰하는 수밖에 없다.

 

 

 

타입 템플릿 매개변수 코드 비대화를 생각해보자
  • 현재까지는 비타입 템플릿 매개변수 코드 비대화에 대해서만 살펴보았지만..
  • 사실 타입 매개변수 역시 코드 비대화의 원인이 될 수 있다.
  • 생각해 볼만한 비타입 템플릿 매개변수 코드 비대화 상황 
    1. 상당수의 플랫폼에서 int와 long은 이진 표현 구조가 동일하다.
      • 그러므로 vector<int>와 vector<long>의 멤버 함수는 서로 빼다 박은 듯 똑같이 나올 수 있을 것이다.
      • 즉, int 및 long에 대해 인스턴스화되는 템플릿들은 '어떤 환경' 에서는 코드 비대화를 일으킬 수 있다.
        • 링커가 똑똑하게 동일 표현구조를 합쳐주기도 하기 때문이다.
    2. 포인터 타입을 매개변수로 취하는 동일 계열 템플릿들은 이진 수준에서만 보면 멤버 함수 집합을 한 벌만 써도 될 것이다.
      • 예컨대 list<int*>, list<const int*>, list<SquareMatrix<long, 3>*> 등.
      • 타입 미정(untyped) 포인터(void*)로 동작하는 버전을 구현 및 사용하여 코드 중복을 없앨 수 있을 것이다.
      • vector, deque, list 등, C++ 표준 라이브러리에서 실제로 사용하는 방법이다.
728x90
Comments