행렬 0x04: C++ Matrix 구조체 디자인

Matrix.h 행렬 구조체 만들기

C++로 Matrix를 구현하기 위해, class로 만들건지, struct로 만들건지 고민할 수 있다. C++에서는 classstruct는 동일하다.(컴파일러도 똑같이 처리한다) 단 하나의 차이는 Struct는 접근 지정자가 기본적으로 public이다. 둘은 동일한 기능을 가지고 차이는 거의 없지만 접근 지정자가 public인 점과, C Style의 구조체 관례때문에, C++에서도 관습적으로 struct는 멤버데이터만을 가진 자료형으로 쓰인다. 이 말은 struct는 C처럼 POD(Plain Old Data)로써 memcpy로도 데이터를 복사할 수 있어야함을 의미한다.

1. POD로 구현하기

우선 결론부터 이야기하자면 POD가 쓰이는 이유는 성능을 위해 memcpy를 쓸 수 있도록하기 위함이다.

POD(Plain Old Data)란, 인스턴스의 메모리 레이아웃이 모두 연속적인 바이트 열을 가짐을 의미한다. 즉, 멤버데이터 중에 힙메모리를 가르키고 힙메모리에 데이터를 확장하는(class같은) 데이터는 없어야한다는 것이다. 이러한 POD는 Old라는 단어를 가진 것 처럼, 개체지향, 모던 프로그래밍에서 안쓰이는 건가? 싶을 수 있다. 하지만 하드웨어 레벨에서 개체를 복사할 때 더 효율적인 측면이 있기 때문에, 런타임 다향성이나 모던스러운 문법, 기술이 필요하지 않거나, 혹은 더 빠른 성능의 코드가 필요할 땐, POD가 빛을 발한다. POD는 복잡한 고민없이 데이터 자체로만 취급하고 그대로 복사(std::memcpy)할 수 있기 때문이다.

POD로 구현하기 위해 다음 조건이 필요하다

  • 표준 레이아웃 타입 표준 레이아웃 타입은 C와 동일한 메모리 레이아웃을 가질 수 있는 타입이다. 다음과 같은 특성을 가진다
    • 가상함수를 가지지 않는다.
    • 모든 멤버 데이터(static 변수 제외)가 표준 레이아웃 타입이다.
    • 가상 부모 클래스를 가지지 않는다.
    • 모든 부모 클래스가 표준 레이아웃 타입이다.
    • 첫번째 비정적 멤버로 부모 클래스와 동일한 타입의 데이터를 갖지 않는다.(메모리 정렬, 패딩 이슈)
    • 레퍼런스 멤버를 갖지 않는다.
    • 단 하나의 부모클래스를 가져야한다.
    • 부모클래스는 멤버 데이터가 없어야한다.
    • 모든 멤버 데이터는 같은 접근제한을 가진다.
    • memcpy가 가능하다.
    • C와 호환된다.
  • trival 타입 생성자, 소멸자, 복사 생성자, 이동 생성자가 컴파일러가 만드는 형태라면 trival 타입이다. 즉 간단한(trival) 복사가 가능하다.
    • 생성자 소멸자가 정의되지 않았다.
    • 가상함수가 없어야 한다.
    • 가상 부모 클래스가 없어야 한다.
    • 모두 trival한 멤버 데이터야한다.
    • memcpy가 가능하다.
    • C와 호환되지 않을 수 있다.

class/struct가 POD인지 확인할려면 <type_traits>std::is_pod를 사용하면 된다.

정리하자면

  1. 메모리 레이아웃이 간단해야한다.
  2. 복사 관련 의가 없어야한다.

로 간단히 설명할 수 있다.

2. 제네릭 프로그래밍

제네릭 프로그래밍을 남용하는 것은 좋지 않지만 행렬 구조체를 만드는데 제네릭 프로그래밍을 매우 적절한 경우가 될 수 있다. 타입에 의존되지 않고 재사용성과 유연성을 높일 수 있어 복소수나 특정한 자료형을 사용할 때 유용할 수 있다. 따라서 제네릭 프로그래밍을 하기 위해선 함수와 구조체를 일반화하여 설계해야한다. 교육적인 목적으로도 좋은 시도가 될 수 있다. 제네릭하게 제대로 구현했다면 복소수 타입의 제네릭 인수를 사용하는데 문제가 없을 것이다.

3. 일관된 함수 디자인

함수가 입력된 값을 처리하는 방식은 크게 두가지 방식이 있다.

  • 누산 함수 (Accumulator Function) 누산 함수는 함수를 사용함으로써, 개체의 상태를 갱신해 나가거나, 입력값으로 들어온 개체의 상태를 갱신해 나가는 방식이다.
    Matrix<float, 2, 2> m = {0, 0, 0, 0};
    m.Add({1, 2, 3, 4}); // m 개체의 상태를 갱신함.
    
  • 순수 함수 (Pure Function) 순수 함수는 입력된 레퍼런스나 개체를 갱신하지 않고 새로운 값을 반환하는 함수를 의미한다. 함수 호출에 있어 사이드 이펙트가 없고, 등일한 입력에 대해 동일한 출력을 반환한다.
    Matrix<float, 2, 2> test1 = {0, 0, 0, 0};
    Matrix<float, 2, 2> test2 = {1, 1, 1, 1};
    // test1, test2에 상태를 갱신하지 않고 새로운 값을 반환
    Matrix<float, 2, 2> result = addMatrix(test1, test2);
    

일관된 멤버 함수 디자인이 중요한 이유는 Add함수는 누산 함수고, Sub함수는 순수 함수라면 사용할 때 매우 혼란스러울 수 있다. 따라서 멤버 함수가 어떤 유형의 함수를 사용하기로 했다면 일관성있게 모든 함수를 동일한 유형으로 통일하는 것이 좋다.

이번 프로젝트에서는 시그니처를 가진 멤버 함수는 가능한 누산 함수로 작성하고, +, -와 같은 연산자 오버로드에서는 새로운 값을 반환하는 순수 함수의 유형으로 작성할 계획이다.

그리고 누산 함수는 새로운 값을 반환하지 않기 때문에 성능에 더 도움이 된다.

Matrix 프로토타입


template <class K, u64 N, u64 M>
struct Matrix
{
    K data[N * M];

    K& operator()(u64, u64);
    const K& operator()(u64, u64) const;

    void Add(const Matrix&);
    void Sub(const Matrix&);
    void Scl(const K&);
    // etc...
};

template <class K, u64 N, u64 M>
Matrix<K, N, M> operator+(const Matrix<K, N, M>&, const Matrix<K, N, M>&);
template <class K, u64 N, u64 M>
Matrix<K, N, M> operator-(const Matrix<K, N, M>&, const Matrix<K, N, M>&);
template <class K, u64 N, u64 M>
Matrix<K, N, M> operator*(const Matrix<K, N, M>&, const K&);
template <class K, u64 N, u64 M>
Matrix<K, N, M> operator*(const K&, const Matrix<K, N, M>&);
//etc...

Matrix 구조체는 POD타입으로 설계되었으며, 제네릭 프로그래밍 기법을 활용하였다. 또한 일관된 함수디자인을 통해 코드의 가독성과 유지보수를 높이고자 한다.

Categories:

Updated:

Leave a comment