[VC/Graphics] SIMD를 사용하여 행렬 곱셈 가속하기

한번 SIMD를 사용해서 행렬곱셈을 해 봤습니다.
있는 코드가 있으면 편하게 긁어다 붙이려 했는데 구글을 아무리 뒤져도 SIMD를 이용해서 행렬*벡터만 보이고 행렬*행렬을 하는 코드가 안 보이길래 만들어버렸군요. 혹시 영어로 검색하는 사람들을 위해서 영어로도 써 봅니다.
Optimized matrix multiplication using SIMD
정도면 되려나요.


* SIMD

여러개의 계산을 병렬처리하는 방법으로 아마 펜티엄 2인가 3에서 128비트 레지스터에 부동소수점 4개를 올려서 한방에 계산하는 뭐 그런게 나왔던것 같습니다. 서로 의존성이 없는 값들을 계산하기에 적합한데 특히 그래픽스에서 행렬과 벡터를 다룰 때 유용하게 사용할수 있습니다.


* 아이디어

기본적으로 SIMD를 이용하면 실수 4개의 덧셈 혹은 곱셈을 한번에 할 수 있습니다.
행렬 계산식을 잘 보면 다음의 두 행렬을 곱한다고 쳤을 때
( a11 a12 a13 a14 )   ( b11 b12 b13 b14 )
( a21 a22 a23 a24 )   ( b21 b22 b23 b24 )
( a31 a32 a33 a34 )   ( b31 b32 b33 b34 )
( a41 a42 a43 a44 )   ( b41 b42 b43 b44 )

두 행렬 A, B를 곱해서 나오는 행렬 C의 첫행은

c11 = a11 * b11 + a12 * b21 + a13 * b31 + a14 * b41
c12 = a11 * b12 + a12 * b22 + a13 * b32 + a14 * b42
c13 = a11 * b13 + a12 * b23 + a13 * b33 + a14 * b43
c14 = a11 * b14 + a12 * b24 + a13 * b34 + a14 * b44

이므로

중간중간에 더하기를 기준으로 잘라보면

(c11, c12, c13, c14) =
(a11 * (b11, b12, b13, b14)) +  
(a12 * (b21, b22, b23, b24)) +  
(a13 * (b31, b32, b33, b34)) +  
(a14 * (b41, b42, b43, b44))

가 됩니다.

여기서 SIMD를 사용하면 a11 * (b11, b12, b13, b14) 를 한 명령어로 처리할 수 있으므로 속도 증가를 기대할 수 있습니다.



* VC 라이브러리

VC에서는 SIMD를 위해서 쓰기 편하게 랩핑된 클래스를 제공합니다. 인라인 어셈이나 알아보기 불편한 함수 셋트가 있긴 한데 편한걸 두고 굳이 힘들게 쓸 이유는 없겠죠;
VC에서 128비트 레지스터에 올라갈 변수는 __m128형입니다. 단순히 float[4]라고 보시면 되겠네요. 한편 SIMD계산을 위한 클래스는 F32vec4입니다. F32vec4는 <fvec.h>를 인클루드해야만 사용가능합니다.



* 코드

struct CMatrix
{
    union
    {
       struct 
       {
            __m128 _L1, _L2, _L3, _L4;
       };
       struct 
       {
            float _11, _12, _13, _14;
            float _21, _22, _23, _24;
            float _31, _32, _33, _34;
            float _41, _42, _43, _44;
        };
    };
    void    MultiplyWith(const CMatrix& mat)
    {
        F32vec4 t1, t2, t3, t4;

        t1  = mat._L1 * F32vec4(_11);
        t1 += mat._L2 * F32vec4(_12);
        t1 += mat._L3 * F32vec4(_13);
        t1 += mat._L4 * F32vec4(_14);

        t2  = mat._L1 * F32vec4(_21);
        t2 += mat._L2 * F32vec4(_22);
        t2 += mat._L3 * F32vec4(_23);
        t2 += mat._L4 * F32vec4(_24);

        t3  = mat._L1 * F32vec4(_31);
        t3 += mat._L2 * F32vec4(_32);
        t3 += mat._L3 * F32vec4(_33);
        t3 += mat._L4 * F32vec4(_34);

        t4  = mat._L1 * F32vec4(_41);
        t4 += mat._L2 * F32vec4(_42);
        t4 += mat._L3 * F32vec4(_43);
        t4 += mat._L4 * F32vec4(_44);


        _L1 = t1; _L2 = t2; _L3 = t3; _L4 = t4;
    }
};




* 주의사항

__m128의 경우 반드시 16byte로 정렬된 메모리에 올라가 있어야만 합니다. 내부적으로 __declspec(align(16)) 을 사용하는데, 이렇게 정렬된 메모리상에 있는 변수의 경우 함수의 인자로 넘길때 값에 의한 (call-by-value) 전달은 허용되지 않습니다. 보통은 const 레퍼런스를 넘기를 방법으로 회피할 수 있죠.
또한 std::vector를 사용할 때 애로사항이 꽃피는데 VC의 std::vector 구현 중 resize() 내부에서 신나게 값에 의한 전달을 하기 때문입니다. 이 현상을 가장 간단하게 회피하는 방법은 std::vector를 대신하는 전용 컨테이너를 만드는것이죠. 이때 주의할 점은 단순히 new로 CMatrix를 할당했을 경우 정렬된 메모리를 얻을 수 없기 때문에 _L1에 엑세스하려는 순간 access violation error가 뜨면서 코드가 죽는 문제가 생깁니다.
이 문제를 회피하기 위해서는 new, new[], delete, delete[]를 모두 오버로드해서 내부적으로 _aligned_malloc()과 _aligned_free()를 사용하도록 신경써야 합니다. 또한 CMatrix를 포함하고 있는 클래스가 있을 경우 그 또한 정렬된 메모리상에 올라가야 합니다. 당연한 얘기죠?



* 퍼포먼스

제 테스트로는 제 CPU(P4 E6300 듀오) 에서 단순히 곱셈/덧셈으로 구현한 행렬곱보다 80-90%정도의 성능향상이 있었습니다.



* 참고자료

http://www.gamasutra.com/features/20000131/barad_03.htm
http://www.gamedev.net/community/forums/topic.asp?topic_id=526505


by netcrawler | 2009/05/04 02:24 | 취미 | 트랙백 | 덧글(7)

트랙백 주소 : http://netcrawler.egloos.com/tb/4130839
☞ 내 이글루에 이 글과 관련된 글 쓰기 (트랙백 보내기) [도움말]
Commented by JM at 2009/05/04 11:03
이건 무슨 빵 얘긴가요 ^^?
Commented by netcrawler at 2009/05/12 23:20
넵 코드빵?
Commented by 달곰 at 2009/05/04 11:40
...공대생의 비극이로고
Commented by 달곰 at 2009/05/04 11:40
아니 잠깐 넌 문과생..
Commented by netcrawler at 2009/05/12 23:20
그러게 내가 왜 이런걸 하고 있지 (...)
Commented by 치아쿠 at 2009/05/04 14:57
오 멋진 코드군요. 음.. 행렬이 커질때를 대비해서 11, 12, 13 이런것도 수치로 계산해볼수 있겠네요. 11을 i로 두고 뒤에는 i+1 식으로 상대값으로 넣어준다면 행렬이 x by x가 되어도 할수 있지 않을까요? 음. 만들어 볼까나...
Commented by netcrawler at 2009/05/12 23:22
글쎄요 이 인스트럭션 자체가 4개짜리 벡터계산에 최적화되어서 나온지라 임의 사이즈의 행렬계산을 이런식으로 가속할 수 있을지는 잘 모르겠네요. 퍼포먼스에 대단히 민감한 부분이 아니라면 for돌려서 구현하는 편이 (컴파일러의 최적화를 믿으면) 제일 낫지 않나 싶습니다.

:         :

:

비공개 덧글

◀ 이전 페이지 다음 페이지 ▶