이것은 1999년 하이텔 게임 제작 동호회에 올렸던 강좌 입니다.

#####################################################

안녕하세요.. 안영기입니다.

다름 아니라 이번에는 MMX Technology 를 이용하여 3D 에 사용되는 고속 행렬
연산을 설명하고자 합니다.

흔히 3D 게임을 만들 때 가장 기본이 되는 연산이며 확대, 축소, 이동, 각도
변화등등에 걸쳐 다용도로 쓰이는 부분입니다.

MMX 가 탄생한 배경이 빠른 Multimedia 지원이니 많큼 이런쪽에서도 지원이
되는데요...

이에, 제가 소개 하고자 하는 팁은 MMX 를 코드를 이용하여 그 행렬 계산을
빠르게 해보자는 의도입니다.

풀소스에 들어가기 전에 핵심 부분만 추려보면 다음과 같습니다.


-------------------------------------------------------------------------

 >> MMX 이론

구체적인 방식이라고 말한다면 MMX 명령어인 'pmaddwd'라는 명령어에 있습니다.
이 명령어는 여타의 DSP 칩에서와 같이 곱하고 동시에 더하는 연산을 합니다.
                                     ~~~~~~~~~~~~~~~~~~~~~~~~~
그리고 한번에 하나의 곱셈이 아닌 동시에 word 형 데이터 4개를 곱하고
더합니다.

그렇다면 감이 오시겠죠 ?

3D 게임에서 사용하는 행렬은 4-3 를 주로 사용하고 4-3 끼리 곱하는 연산을
하기 위해서 4-4 행렬로 확장을 해서 사용하게 됩니다. 물론 마지막 값은
항상 { 0, 0, 0, 1 }로 둡니다.

( 4-3 과 4-3 끼리는 절대 곱해질수 없죠. 고교 정석 수학 책을 참고 하시길..)

그리고 제가 이번에 실험하고자 하는 행렬식을 본다면...

  | x' |     |  a1  a2  a3  a0  | | x |
  | y' |  =  |  b1  b2  b3  b0  | | y |
  | z' |     |  c1  c2  c3  c0  | | z |
  | w' |     |  p1  p2  p3  p0  | | 1 |

이렇습니다.. 또한,

        x' = a1 * x +  a2 * y + a3 * z + a0

로 계산이 됩니다.

새로운 x 좌표인 x' 를 계산하기 위해서는 총 8개의 인자를 짝을 맞춰
곱하고 그 4개의 값을 더하는 형식입니다.

즉 이 방식이 바로 pmaddwd 명령어와 동일하죠 ?

그럼 pmaddwd 의 구체적인 동작을 얘기해 보겠습니다.

              1    2    3    4    5    6    7    8    <- 각 byte별

    mm0 =    (  w1  )  (  w2  )  (  w3  )  (  w4  )

    mm1 =    (  w1' )  (  w2' )  (  w3' )  (  w4' )

가 들어 있을 때 ...

    pmaddwd mm0, mm1

이란 명령을 실행하면..

              1    2    3    4    5    6    7    8    <- 각 byte별

    mm0 =    ( w1*w1' + w2*w2')  ( w3*w3' + w4*w4')

이렇게 word 가 DWORD 로 확장이 되면서 더해집니다.(mm0 레지스터는 8bytes짜리)

그리고 상위 4bytes 와 하위 4bytes 를 더하면 되는데..

    movq   mm1, mm0
    psrlq  mm1, 32
    paddd  mm0, mm1
    movd   eax, mm0

이렇게 하면 최종 결과인 w1*w1' + w2*w2' + w3*w3' + w4*w4' 가 eax 에 들어가게
됩니다.

위에 있는 MMX 레지스터끼리의 대입이나 쉬프트는 모두 1 클럭만 소비하므로
빠른 속도로 행렬 계산을 해낼수가 있는 것입니다.


-------------------------------------------------------------------------

 >> 구체적인 코드로 설명

아래의 코드는 볼랜드 C++ 빌더에 의해 작성되었습니다.
VC++ 유저들은 약간의 수정을 하셔야 할겁니다.

위의 함수는 C = A * B 라는 행렬을 계산하기 위해 임의로 만들어진 예제식이며
( A = 4*4,  B = 4*1,  C = 4*1 )

   short A[4][4];
   short B[4];
   short C[4];

   MMX_Matrix(A[0],B,C);

위와 같은 방식으로 사용하시면 C[] 에 결과가 돌아오는 방식입니다.

원래 우리들은 장황한 이론보다는 소스코드의 주석에 더 빨리 머리가 반응하므로
바로 소스에 주석을 다는 식으로 설명하겠습니다.


void __fastcall MMX_Matrix(short *A, short *B, short *C)
{

   asm  mov     esi, eax        // A 의 포인터 주소를 esi에

   asm  mov     ebx, edx        // B 의 포인터 주소를 ebx에
   asm  movq    mm2, [ebx]      // mm2 에 B[]의 word형 인수 4개를 저장

   asm  mov     edi, ecx        // C 의 포인터를 edi에

   asm  pxor    mm4, mm4        // mm4 레지스터 초기화
   asm  mov     eax, 0000FFFFh
   asm  movd    mm4, eax        // mm4 <- 0000 0000 0000 FFFF

   // 1 행
  asm  movq    mm0, [esi]      // A[0]의 4개의 word형 인수를 저장
   asm  movq    mm1, mm2        // mm1 을 B 의 값으로

   asm  pmaddwd mm0, mm1        // 문제의 연산, 설명은 위에서 했다..
   asm  movq    mm1, mm0        // 현재 곱하고 더한 결과가 4bytes씩 분리되어
   asm  psrlq   mm1, 32         // 있으므로 상위와 하위 4bytes 를 더해서
   asm  paddd   mm0, mm1        // mm0 레지스터에 저장
   asm  pand    mm0, mm4        // mm3 는 하위 2bytes 만 남게함
   asm  movq    mm3, mm0        // mm3 의 C[0] 위치에 저장

   // 2 행
   asm  movq    mm0, [esi+8]    // A[2]의 4개의 word형 인수를 저장
   asm  movq    mm1, mm2

   asm  pmaddwd mm0, mm1
   asm  movq    mm1, mm0
   asm  psrlq   mm1, 32
   asm  paddd   mm0, mm1
   asm  pand    mm0, mm4
   asm  psllq   mm0, 16
   asm  por     mm3, mm0        // mm3 의 C[1] 위치에 저장

   // 3 행
   asm  movq    mm0, [esi+16]
   asm  movq    mm1, mm2

   asm  pmaddwd mm0, mm1
   asm  movq    mm1, mm0
   asm  psrlq   mm1, 32
   asm  paddd   mm0, mm1
   asm  pand    mm0, mm4
   asm  psllq   mm0, 32
   asm  por     mm3, mm0        // mm3 의 C[2] 위치에 저장

   // 3 행
   asm  movq    mm0, [esi+24]
   asm  movq    mm1, mm2

   asm  pmaddwd mm0, mm1
   asm  movq    mm1, mm0
   asm  psrlq   mm1, 32
   asm  paddd   mm0, mm1
   asm  pand    mm0, mm4
   asm  psllq   mm0, 48
   asm  por     mm3, mm0        // mm3 의 C[3] 위치에 저장

   asm  movq    [edi], mm3      // mm3 의 내용을 C[] 에 복사

   asm  emms                    // FPU 상태 비움

}

pmaddwd 명령의 용도만 알면 쉽게 이해가 될만한 내용이라 따로 설명은 하지
않겠고 4개의 행을 각각 풀어쓴것은 그나마 속도가 조금이나마 빨라지지
않겠나라는 생각에 한것이며 평소때의 경험으로 볼때 4번의 루프로 돌려도
속도 저하는 거의 없을 것으로 보아집니다.


-------------------------------------------------------------------------

 >> 퍼포먼스

일단 가장 중요한게 얼마나 빠르냐 ? 라는 것입니다.
빠르지 않다면 구태여 이런것을 사용할 필요도 없고 사용해서도 안될것입니다.

위의 코드의 MMX_Matrix()를 사용한 루틴은

   short A[4][4] = {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16};
   short B[4]    = {11,22,33,44};
   short C[4];

   DWORD StartTime = timeGetTime();

   for (int i = 0; i < 1000000; i++)
      MMX_Matrix(A[0],B,C);         // 구체적인 용법임....

   Edit1->Text = IntToStr(timeGetTime() - StartTime);

라는 코드를 통해 (4*4) * (4*1) 계산을 동시에 1000000번 수행 하엿고,

일반 행렬계산은

void __fastcall Matrix(short *_A, short *B, short *C)
{

   short *A = _A;

   for (int i = 0; i < 4; i++) {
      C[i] = A[0] * B[0] + A[1] * B[1] + A[2] * B[2] + A[3] * B[3];
      A += 4;
   }
}

라는 함수로 호출하게 했습니다.
( 왜 최적화가 안되었냐구요 ? 당연히 MMX와 최대한 속도차가 나게 하기 위해서! )

그리고 결과는 다음과 같습니다.

==============================================================================


일반 행렬 계산식으로 계산    :  1000000 번 호출하는데  평균 0.483 초
                                                       ~~~~~~~~~~~~~

MMX 행렬 계산식으로 계산:  1000000 번 호출하는데  평균 0.222 초
                                                       ~~~~~~~~~~~~~

( 펜티엄 2 266MHz 에서 테스트.. )

==============================================================================


약 2 배 이상의 성능 향상을 보이는 것으로 결과가 나왔습니다.

이제는 단순 행렬 계산식인 위의 방식을 자신에게 맞는 3D 용 행렬 계산에
적용시키는 일이 남았군요..


그리고 트루컬러에서 MMX 알파 블랜딩 역시도 위의 소스와 거의 같은 것이므로
그것에 대한 강좌는 생략하도록 하겠습니다..

그렇다면 다음 강좌는 Direct Music 이 되겠군요...

----------------------------------------------------------------------------

             " A Mountain is a mountain, Water is water "


                                              copyright SMgal 1999/05/28