2011년 8월 24일 수요일

C++로 간단하게 구현해보는 희소행렬

C++에서 STL을 이용하여 희소행렬(sparse matrix)을 구현한다면, 원소를 저장하기에 가장 좋은 자료형은 뭘까? 아마 map일 거다. 키를 위치 인덱스로 하고, 데이터에는 실제 값을 넣으면 된다.

STL엔 두 개의 map이 있다. 하나는 std::map이고, 다른 하나는 std::unordered_map이다. 이들의 용도는 다음과 같이 간단히 정리할 수 있다. 킷값에 따라 정렬해주는 map이 필요하다면 std::map을 사용하고, 빠른 검색이 필요하다면 std::unordered_map을 사용한다. 우린 키를 정렬할 필요는 없으므로, std::unordered_map을 이용하기로 하자.

이제 2차원 인덱스를 표현할 자료형을 결정해야 한다. C++의 내장 배열을 사용하자니 번거롭고, std::vector를 사용하자니 너무 무겁다. 2011년 8월 12일에 제정된 새로운 C++ 표준에선 std::array를 정의하고 있다. 이건, 고정된 크기의 std::vector라고 생각하면 되는데, std::vector보다 가볍고 C++ 내장 배열보다 편리하다. 행렬의 인덱스는 길이가 2로 고정되므로 std::array는 인덱스의 자료형에 딱 맞는 컨테이너이다.

이제 구현을 해보자. 먼저, 필요한 헤더를 불러온다.
// smat.cc
#include <array>
#include <cstdlib>
#include <iostream>
#include <numeric>
#include <unordered_map>
cstdlib 헤더는 main 함수의 반환 값으로 사용할 EXIT_SUCCESS 매크로를 정의가 들어 있고, numeric 헤더는 해시 값 생성에 사용할 accumulate 함수의 선언이 들어 있다.

다음으로 할 일은 함수자(functor)를 만드는 것이다. std::unordered_map해시표를 사용하는 데, 키로부터 해시 값을 계산해서 해시표를 생성해야 한다. 기본 자료형에 대해서는 해시 값을 생성하는 함수자가 이미 선언되어 있지만, 그 외의 자료형에 대해선 우리가 직접 만들어줘야 한다. std::unary_function을 이용해서 간단하게 인덱스의 두 값을 더하는 해시 함수자를 만든다.
namespace std
{
  template <>
  struct hash<array<int, 2> >: public unary_function<array<int, 2>, size_t>
  {
    size_t operator()(const array<int, 2>& idx) const
    {
      return accumulate(idx.begin(), idx.end(), 0);
    }
  };
}
이제 희소행렬 자체를 구현할 클래스를 선언한다. 행렬 연산을 구현하는 여러 함수도 구현해야겠지만, 여기선 행렬 원소의 읽기와 쓰기에 관련된 연산자 정도만 구현해보자.
template <typename T> class SparseMatrix
{
public:
  SparseMatrix(int i, int j): i_size(i), j_size(j) {}

  T& operator()(int i, int j)
  {
    std::array<int, 2> idx;
    idx[0] = i;
    idx[1] = j;
    return mat[idx];
  }

  T operator()(int i, int j) const
  {
    std::array<int, 2> idx;
    idx[0] = i;
    idx[1] = j;
    typename std::unordered_map<std::array<int, 2>, T>::const_iterator tmp;
    tmp = mat.find(idx);
   
    if (tmp == mat.end())
      return T();
    else
      return tmp->second;
  }
   
private:
  std::unordered_map<std::array<int, 2>, T> mat;
  std::size_t i_size, j_size;
};
눈여겨볼 점은 행렬의 원소를 설정할 때 사용할 operator()와 원소를 조회할 때 사용할 operator() const를 모두 선언해야 한다는 것이다. operator()만 선언한다면, 행렬의 원소를 조회할 때마다 조회하는 인덱스에 해당하는 항목을 map에 만들게 된다. 예를 들어,
SparseMatrix<double> sm(3, 3);
for (int i = 0; i < 3; ++i) {
    for (int j = 0; j < 3; ++j) {
      std::cout << sm(i, j) << " ";
    }
    std::cout << std::endl;
  }
처럼 모든 원소를 조회하기만 해도 내부 저장 장소인 mat에 모든 원소에 해당하는 항목이 만들어져 메모리를 낭비하게 된다.

실제로 원소를 설정하고 조회하는 방법은 아래와 같다.
using namespace std;

int main(int argc, char* argv[])
{
  SparseMatrix<double> sm(3, 3);
  sm(1, 2) = 1;
  sm(2, 0) = 2;

  const SparseMatrix<double>& sm_ref = sm;
  for (int i = 0; i < 3; ++i) {
    for (int j = 0; j < 3; ++j) {
      cout << sm_ref(i, j) << " ";
    }
    cout << endl;
  }

  return EXIT_SUCCESS;
}
operator() const를 호출하려면, 상수 레퍼런스나 상수 포인터를 이용해서 SpareMatrix에 접근해야 한다. 뭐, 대충 이렇다. 이 코드는 C++0x 표준인  std::arraystd::unordered_map을 사용하는데, 이 기능이 시험적으로 구현된 거라서 이용하면 컴파일 할 때 -std=c++0x 옵션을 줘야 한다.
$ g++ -Wall -g -pedantic -std=c++0x -o smat smat.cc
실행해보면, 잘 작동하는 것을 확인할 수 있다.
$ ./smat
0 0 0
0 0 1
2 0 0
$
mat의 크기를 검사해 보면, 단 두 개의 항목만 저장하고 있는 것을 확인할 수 있다. 간단한 희소행렬 완성!

2011년 8월 23일 화요일

C++의 반복자(iterator)를 이용하여 파이썬 생성자(generator) 만들기

최근에 GMES의 C++ 모듈 설계를 변경하면서, 내부 자료형으로 STL의 컨네이너를 사용하는 클래스가 필요하게 되었다. 이 글에서 설명하려는 내용과 관계 없는 부분을 제거하면, 대략적인 형태는 아래와 같다.
// itergen.hh
#include <vector>

class A
{
public:
  A(): a(3)
  {
    for (size_t i = 0; i < a.size(); i++)
      a[i] = i;
  }

  std::vector<int>::const_iterator
  begin() const
  {
    return a.begin();
  }

  std::vector<int>::const_iterator
  end() const
  {
    return a.end();
  }

private:
  std::vector<int> a;
};
// itergen.cc
#include "itergen.hh"
이 코드는 파이썬으로 래핑하여 사용하게 되는데, 아래와 같이 생성자로 이용하고자 한다. 어떻게 해야 할까?
>>> for i in A():
...    print i
1
2
3
파이썬 생성자에 관한 API를 살펴봤는데, 이거 잠깐 보고 구현할 난이도는 아닌 것 같다. 이왕 SWIG를 이용한다면, 뭔가 더 간단한 방법이 없을까? Reference Man의 블로그에 방법이 나와 있다. 이 블로그에 나와 있는 내용을 간단하게 정리해 보자.

일단, C++ 클래스에선 시작과 끝 부분의 반복자를 반환하는 begin, end 메서드만 제공하면 된다. 이는 일반적으로 STL에서 사용하는 방식이므로 C++ 클래스에서 제공하는 것이 자연스럽다. C++ 코드에서 할 일은 여기까지다. SWIG가 제공하는 %extend 키워드를 사용하면 C++ 코드를 더럽히지 않고도 파이썬 래핑에 필요한 코드를 추가할 수 있다. swig의 인터페이스 파일은 아래와 같다.
// itergen.i
%module itergen

%{
#include "itergen.hh"
%}

%extend A {
  std::vector<int>::const_iterator* _begin()
  {
    return new std::vector<int>::const_iterator($self->begin());
  }

  std::vector<int>::const_iterator* _end()
  {
    return new std::vector<int>::const_iterator($self->end());
  }

  int _deref(std::vector<int>::const_iterator* it)
  {
    return **it;
  }

  bool _compref(std::vector<int>::const_iterator* lhs,
  std::vector<int>::const_iterator* rhs)
  {
    return *lhs == *rhs;
  }

  void _incref(std::vector<int>::const_iterator* it)
  {
    ++(*it);
  }

  %pythoncode %{
    def __iter__(self):
      it = self._begin()
      while not self._compref(it, self._end()):
          yield self._deref(it)
          self._incref(it)
  %}
};

%include "itergen.hh"
파이썬은 STL의 반복자를 제대로 다루지 못하므로, 이를 처리해 주는 모든 메서드를 C++ 측에서 만들어줘야 한다. C++ 코드를 수정할 것 없이 인터페이스 파일에서 %extend 키워드를 이용해서 관련 메서드를 추가한다.

파이썬 클래스는 __iter__ 메서드를 선언해주면, 생성자로 이용할 수 있다. 이 메서드는 앞에서 만들어준 C++ 메서드를 이용하여 C++의 반복자를 처리한다. 원리는 간단하다. STL에서 흔히 사용하는, 반복자를 이용한 컨테이너 순회를 이용하는 데, 차이점이라면 값의 반환으로 return 대신에 yield를 사용한다는 점이다. yield를 사용하면, 함수의 값은 반환되는 데, 함수는 종료되지 않고 next 메서드가 호출될 때마다 yield를 호출한 다음 지점부터 연속해서 실행한다. __iter__ 메서드는 %pythoncode 키워드를 이용하여 파이썬 프록시 클래스에 추가한다.

컴파일 과정은 아래와 같다. 인터페이스 파일로부터 프록시 클래스와 파이썬 인터페이스를 생성하고, C++ 코드를 컴파일 한 후, 공유 라이브러리로 만들어 주면 된다.
$ swig -c++ -python itergen.i
$ g++ -Wall -g -I/usr/include/python2.6 -c itergen_wrap.cxx
$ g++ -shared itergen.o itergen_wrap.o -o _itergen.so
이제 파이썬 해석기에서 모듈을 올리고 생성자를 사용해보자.
$ python
Python 2.6.5 (r265:79063, Apr 16 2010, 13:09:56)
[GCC 4.4.3] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import itergen
>>> a=itergen.A()
>>> iter(a)
<generator object __iter__ at 0xb7720234>
>>> for i in a: print i
...
0
1
2
>>>
완료!