dev.log2009. 2. 17. 00:00
행렬을 구현하는 데에 있어서 가장 어려운 것은, 바로 메모리상의 배열 우선순위(major ordering)를 정하는 것이다. 단위 배열로 행벡터를 쓸 것인가 열벡터를 쓸 것인가를 결정하는 것인데, 행벡터는 행렬에 있어서 같은 행번호를 가진 원소가 서로 인접하도록 메모리 상에 배치하는 것이고, 열벡터는 같은 열 번호를 가진 원소가 서로 인접하도록 메모리 상에 배치하는 것. 어차피 둘 중의 하나 밖에 선택할 수 없기 때문에 심플한 문제이기는 한데, 좀 미묘한 문제가 걸려 있다.

우선, 행우선(row major) 배치는, 다음을 보자.


이와 같은 행렬이 행 우선 배열에서는 메모리 상에 선형으로 다음과 같이 배치된다.

 a00 a01 a02 a03 a10 a11 a12 a13 a20 a21 a22 a23 a30 a31 a32 a33

이는 C/C++의 2차원 배열 선언과 동일한 배치이다.

float a[4][4]

위와같은 선언은 메모리를 다음과 같이 배치한다.

a[0][0] a[0][1] a[0][2] a[0][3] a[1][0] a[1][1] a[1][2] a[1][3] a[2][0] a[2][1] a[2][2] a[2][3]
... a[3][0] a[3][1] a[3][2] a[3][3]

따라서, 배열의 첨자가 그냥 순서대로 [행번호][열번호]가 된다는 편리함이 있다. 행렬의 이름이 '행', '열' 순서이므로 이름과 잘 일치한다. 또한 C/C++의 기본 배열과 개념상 일치해 보인다는 장점이 있고, Direct3D에서 사용하는 방식이므로 별다른 변환 없이 Direct3D에서 사용이 가능하다. 유혹적이다.

반면, 열우선(column major) 배치는 행렬을 메모리에 배열하는 방법이 반대다.

 a00 a10 a20 a30 a01 a11 a21 a31 a02 a12 a22 a32 a03 a13 a23 a33

이를 2차원 배열로 표현한다면 [열번호][행번호]의 순서로 첨자를 부여해야 한다는 점이 C/C++의 표준과 불일치한다는 인상을 준다. 하지만 어차피 가로/세로의 선호는 사람에 따라 달라지는 것이므로 열이 앞이냐 행이 앞이냐 하는 문제는 표준에 정의되어 있지 않은 문제이다. 메모리를 가로 방향으로 나열해서 그렇지, 다음과 같이 나열하면 자연스러워 보인다.

 a00
 a10
 a20
 a30
 a01
 a11
 a21
 a31
 a02
 a12
 a22
 a32
 a03
 a13
 a23
 a33

이는 전적으로 메모리가 위아래로 뻗어있느냐 양옆으로 뻗어있느냐로 보는 것의 차이인데, 사실 어느 쪽을 택해도 무방하다. 물론 행렬을 표현한 2차원 배열에서 첨자의 순서가 '행','열' 순서가 아닌 점은 약간 불편할 것 같긴 하다. 하지만 흔히 쓰는 수학적인 (벡터를 세로로 적고 행렬의 뒤에 곱하는) 표현과 일치한다는 장점이 있고, OpenGL에서 사용하는 오더링이므로 OpenGL에서 별다른 변환 없이 사용이 가능하다.

ice 프로젝트의 요구조건중 하나가 OpenGL과 Direct3D의 동시 지원이었기 때문에, 가능한 방법은 3가지다
1. 행 우선 배치로 구현하고, OpenGL에서는 일일이 transpose하여 로드한다.
2. 열 우선 배치로 구현하고, Direct3D에서는 일일이 transpose하여 로드한다.
3. 둘 다 구현한다.
사실, 둘 다 구현하는게 바람직하긴 한데, 문제는 그렇게 할 경우에 클래스가 2개 생기므로 이름 공간이 더럽혀지는 결과가 초래되는 점이 아름답지 못하다. row_matrix와 col_matrix가 공존하는 구조를 용납할 수는 없는 법.

그래서 전에 벡터 클래스를 만들 때 SSE 스페셜라이제이션을 템플릿 매개변수로 컨트롤했던 것을 약간 이용하여, 역시 템플릿 매개변수로 major ordering을 결정하는 방법을 취하기로 했다.
enum SPECIALIZE_POLICY
{
    SPECIALIZE_DEFAULT,
    SPECIALIZE_SSE,
};

enum MAJOR_ORDER
{
    ROW_MAJOR,
    COLUMN_MAJOR,
};

// OpenGL용 열우선 배열을 기본으로 구현
template <class scalar_t, MAJOR_ORDER order = COLUMN_MAJOR, SPECIALIZE_POLICY policy = SPECIALIZE_DEFAULT >
struct matrix4t
{
    enum { MAJOR_VECTOR = order };
    typedef scalar_t                    scalar_type;
    typedef vector4t<scalar_t, policy>  vector_type;
    typedef vector3t<scalar_t, policy>  coord_type;
    union
    {
        vector_type v[4];
        scalar_type s[16];
    } m;
public:
    matrix4t() {}
    matrix4t(   scalar_type _11, scalar_type _12, scalar_type _13, scalar_type _14,
                scalar_type _21, scalar_type _22, scalar_type _23, scalar_type _24,
                scalar_type _31, scalar_type _32, scalar_type _33, scalar_type _34,
                scalar_type _41, scalar_type _42, scalar_type _43, scalar_type _44 )
                :m.v[0](_11,_21,_31,_41),
                 m.v[1](_12,_22,_32,_42),
                 m.v[2](_13,_23,_33,_43),
                 m.v[3](_14,_24,_34,_44)
    {
    }
    template <class scalar>
    explicit matrix4t( const scalar* a )
        :m.v[0](a[0], a[4], a[8], a[12] ),
         m.v[1](a[1], a[5], a[9], a[13] ),
         m.v[2](a[2], a[6], a[10],a[14]),
         m.v[3](a[3], a[7], a[11],a[15])
    {
    }
};

// Direct3D용 행 우선 배열을 위한 부분 스페셜라이제이션
template < class scalar_t, SPECIALIZE_POLICY policy >
struct matrix4t< scalar_t, ROW_MAJOR, policy >
{
    enum { MAJOR_VECTOR = ROW_MAJOR };
    typedef scalar_t                    scalar_type;
    typedef vector4t<scalar_t, policy>  vector_type;
    typedef vector3t<scalar_t, policy>  coord_type;
    union
    {
        vector_type v[4];
        scalar_type s[16];
    } m;

   
    static const matrix4t zero;
    static const matrix4t identity;
public:
    matrix4t() {}

    matrix4t(   scalar_type _11, scalar_type _12, scalar_type _13, scalar_type _14,
                scalar_type _21, scalar_type _22, scalar_type _23, scalar_type _24,
                scalar_type _31, scalar_type _32, scalar_type _33, scalar_type _34,
                scalar_type _41, scalar_type _42, scalar_type _43, scalar_type _44 )
                :m.v[0](_11,_12,_13,_14),
                 m.v[1](_21,_22,_23,_24),
                 m.v[2](_31,_32,_33,_34),
                 m.v[3](_41,_42,_43,_44)
    {
    }

    template <class scalar>
    explicit matrix4t( const scalar* a )
        :m.v[0](a[0], a[1], a[2], a[3] ),
         m.v[1](a[4], a[5], a[6], a[7] ),
         m.v[2](a[8], a[9], a[10],a[11]),
         m.v[3](a[12],a[13],a[14],a[15])
    {
    }
};

// SSE 템플릿 매개변수는 명시적으로 스페셜라이제이션할 경우에만 허용하도록 막아둠
template< class scalar_t, MAJOR_ORDER order >
struct matrix4t< scalar_t, order, SPECIALIZE_SSE >
{
    scalar_t error[0]; // SSE spcecialization is not allowed unless explicit
};

쓸 때는 어떤 오더링을 쓸 것인지와 SSE 스페셜라이제이션을 쓸 것인지를 명시하면 된다.

matrix4t<float, COLUMN_MAJOR, SPECIALIZE_DEFAULT >

물론, 위와 같이 쓰는 것은 불편하므로, OpenGL 모듈에서는 다음과 같이 타입으로 선언하는 것이 편리.

[render_ogl.h]
typedef matrix4t<float, COLUMN_MAJOR, SPECIALIZE_DEFAULT > matrix4;

물론, Direct3D모듈에서도 비슷하게 타입으로 선언할 수 있다.

[render_d3d.h]
typedef matrix4t<float, ROW_MAJOR, SPECIALIZE_DEFAULT > matrix4;

구현은 두개를 다 해야 하니 빡세도, 쓸때는 골라쓰는 재미가 있을듯; ice 프로젝트의 철학은 '클라이언트 측에서 엔진의 구성요소를 고를 수 있게 한다'는 것이므로.

Posted by uhm
dev.log2009. 2. 11. 01:02
회사에서 하는 금요 세미나. 저번주에는 K군이 크리티컬 섹션에 대해 발표했었다. K군이 발표에 삽질을 좀 했지만, 크리티컬 섹션 문제는 parallelism에서 매우 중요한 개념이므로 정리하고 넘어가자.

크리티컬 섹션이란, 두 개 이상의 쓰레드(혹은 프로세스)가 있을 때, 같은 공유자원에 접근하고자 시도하는 코드 구간을 말한다. 크리티컬 섹션이 왜 치명적인가 하면, 크리티컬 섹션이 아닌 구간에서는 패러럴리즘에 의해 잘못된 결과가 나올 리가 없기 떄문이다. 논리적으로 타당함에도 불구하고 패러럴리즘에 의해 잘못된 결과가 나올 수 있는 부분은 공유자원을 두 개 이상의 쓰레드에서 동시에 접근하려고 시도하는 곳 외에는 없다. 그래서, 다른 쓰레드와의 공유 자원에 접근하려고 하는 부분을 '치명적 구간'이라고 하는 것이다. 일부 번역서에서는 '임계영역'이라고 하기도 하는데, '제대로 관리하지 않으면 오류가 일어나는 부분'이라는 의미가 잘 살아나지 않는 것 같다. 개인적으로는 '치명적 구간'이라고 하기를 좋아하지만, 이래저래 (잘못된) 번역에 익숙해진 한국 프로그래머가 많으므로 난 그냥 '크리티컬 섹션'이라고 한다.

크리티컬 섹션을 제대로 처리해주지 않으면 잘못된 결과가 나오는 것은 전형적으로 다음과 같은 두 쓰레드를 생각하면 된다.

thread a :
int i=0;
void thread_a()
{
    i++;
}

thread b:
extern int i;
void thread_b()
{
    i--;
}

쓰레드 a와 b가 동시에 시작했다고 치면, 두 쓰레드가 종료했을 때 i의 값은 -1, 0, 1의 세 값중 어느 값이라도 가질 수 있게 된다. 이는 C++의 단항 증감연산자가 기계어로는 로드/산술연산/저장의 3가지 단계로 나뉘기 때문이다. 단순하게 생각하자면 A쓰레드의 로드/연산/저장이 순서대로 실행될 때 사이사이에 B쓰레드의 로드/연산/저장이 끼어든다고 보면 된다. 가능한 경우의 수는 많지만 대표적인 결과가 나오는 경우만 보면 다음과 같다.

A로드 > A연산 > A저장 > B로드 > B연산 > B저장 ==>  0
A로드 > B로드 > A연산 > A저장 > B연산 > B저장 ==> -1
A로드 > B로드 > A연산 > B연산 > B저장 > A저장 ==>  1

따라서 크리티컬 섹션 문제를 해결하는 것은 특정 쓰레드의 동작 사이에 다른 쓰레드가 끼어들지 못하도록 어떻게 하느냐 하는 것으로 귀결된다. 대략적으로 크리티컬 섹션 문제의 해결책은 다음과 같이 사용하게 된다.

wait( S )      // wait for entering critical section
..............    // critical section
signal( S )   // allow other threads to enter critical section

OS책에서는 크리티컬 섹션 문제의 해결책은 다음 조건을 만족해야 한다고 나온다. 중요하다.
  • Mutual Exclusion (상호 배제)
  • Progress (진행)
  • Bounded Waiting (유한 대기)
상호 배제는, 크리티컬 섹션 문제의 본질인, 한 쓰레드가 자원 R에 대한 크리티컬 섹션을 수행중일 때에는, 다른 쓰레드가 같은 자원 R에 대한 크리티컬 섹션에 들어가서는 안된다는 뜻이다. 진행 조건은, R에 대한 크리티컬 섹션을 수행중인 쓰레드가 하나도 없을 때에는, 어떤 쓰레드가 R에 대한 크리티컬 섹션에 들어가려고 하면 즉시 진행이 되어야 한다는 조건이다. 유한 대기는 말 그대로, 어떤 쓰레드가 R의 크리티컬 섹션을 수행하려고 할 때에는 다른 쓰레드의 R 크리티컬 섹션 진입이 유한한 횟수만큼만 이루어져야 한다는 조건. 당연한 소리같아보이지만, OS책에 나오는 크리티컬 섹션 문제의 솔루션들은 위 3가지 조건을 만족하게 만들기 까지의 발전과정이다. 언뜻 금방 생각할 수 있는 해결책들은 위 3가지 조건을 모두 만족하기가 쉽지 않다.

크리티컬 섹션 문제 해결에 가장 중요한 개념은 wait와 signal에 atomic operation이 도입되었다는 점이다.atomic operation을 뭐라 번역해야 할지는 모르겠지만, (원자적 연산;같은 구린 번역은 꺼져라!) atom의 원래의미인 '쪼갤 수 없는' 연산이란 뜻이다. (atom을 물질계에 적용한 용어인 '원자'가 어울리는 번역이 아닌 이유) 위에서C++의 단항 증감 연산자가 로드/연산/저장의 3단계로 나뉘어서 배열될 수 있는 데 비해, 아토믹 연산은 항상 단체로몰려다니고, 쪼개져서 배열될 수 없는 연산을 가리키는 개념이다. 현대적인 CPU는 모두 다 CPU의 기본 명령어중에 아토믹 연산이 명시되어 나오므로, OS는 크리티컬 섹션 문제의 해결책에 CPU의 아토믹 명령을 쓸 수 있다. 하드웨어의 도움으로 좀 더 우아한 해결책이 나올 수 있는 것은 자명한 일.

대표적인 해결책은 위대한 컴퓨터 과학자인 Dijkstra가 제안한 세마포다. 세마포의 기본적인 아이디어는 공유자원에 동시에 접근할 수 있는 쓰레드의 갯수를 미리 정해두고 지정된 수의 쓰레드만이 크리티컬 섹션에 진입할 수 있도록 한다;는 것이다. 2개 이상의 쓰레드가 동시에 같은 자원을 공유하는 크리티컬 섹션에 들어갈 수 있으면 counting semaphore, 오직 1개만이 크리티컬 섹션에 진입할 수 있으면 binary semaphore라고 한다. 사실 counting semaphore는 binary semaphore를 이용해서 구현할 수 있으므로 binary semaphore가 더 중요하다.
세마포를 이용한 크리티컬 섹션 문제의 해결책은 크게 두가지 구현방법이 있을 수 있다. 한 가지는 busy waiting, 혹은 spinlock이라고 부르는, 쓰레드가 계속 루프를 돌면서 크리티컬 섹션에 진입하기를 기다리는 방법이고, 다른 한 가지는 쓰레드 자체를 대기 상태로 바꿔버리고 세마포의 대기 큐에 들어가게 만드는 것이다. 각기 일장 일단이 있는데, 스핀락 구현은 크리티컬 섹션 진입에 컨텍스트 스위칭에 들어가는 오버헤드가 없다는 점은 좋지만 싱글 프로세서 시스템에서는 쓸 수 없다는 단점이 있다. (한 쓰레드가 CPU에서 계속 루프를 돌고 있는데 다른 쓰레드가 CPU에 올라갈 수 있을 리가 없다) 반면, 쓰레드를 대기 상태로 바꾸는 해결책은 싱글 프로세서 시스템에서도 별다른 문제 없이 쓸 수 있지만, 컨텍스트 스위칭 비용이 들어가게 된다는 점이 좋지 못하다.
결국, 크리티컬 섹션 진입 빈도에 따라 적절한 방법을 선택하는 것이 중요하다는 말씀.

Posted by uhm
geek.log2009. 2. 9. 16:44
ice 프로젝트에서 사용할 스크립트 언어의 이름을 결정했다.
이름하여, lue Script Language.
lue란 이름은, "Life, the Universe, and Everything"의 약자이지, 절대로 lua를 따라서 지은 것이 아니다. 인생, 우주와 모든 것들을 표현할 수 있는 스크립트 언어를 표방하는 원대한 이름.

언어는 기본 스크립트로 .lue 확장자의 파일을 파싱하며, 컴파일된 바이트코드는 .42 확장자를 가진 파일로 저장하면 좋겠다;
책을 낸다면, 책 표지에는 타월에 커다랗고 친절한 글씨로 'Don't Panic'이라고 적어서 출판하면 딱일듯 ( ..)

Posted by uhm