반응형


이번엔 Vector 클래스를 만들어 보자.

원래는 SSE 명령어를 먼저 정리하고 진행하는게 더 좋을 수도 있지만 Vector 클래스를 만들어보면서

처음나오는 명령어들을 하나씩 정리해 나가는 방법도 괜찮을 것 같아서 그렇게 하기로 했다.
class __declspec(dllexport)    MSVector
{
public:
    MSVector(VOID);                                                
    MSVector(CONST FLOAT x, CONST FLOAT y, CONST FLOAT z);        

public:
    inline     VOID    Set(CONST FLOAT x, CONST FLOAT y, CONST FLOAT z, CONST FLOAT w=1.0f);

    inline     CONST FLOAT    GetLength(VOID);                                                        
    inline     CONST FLOAT    GetSqrLength(VOID);                                                        

    inline     VOID            Nagative();  
    inline     VOID            Normalize();                                                            

    inline     CONST FLOAT    GetAngle(MSVector& v);                                                    

    inline     VOID            Difference(CONST MSVector& u, CONST MSVector& v);                        

    inline     CONST FLOAT    Dot(CONST MSVector& v);                                                    
    inline     VOID            Cross(CONST MSVector& u, CONST MSVector& v);                            

public:
    VOID            operator += (CONST MSVector& v);
    VOID            operator -= (CONST MSVector& v);
    VOID            operator *= (CONST FLOAT f);
    VOID            operator /= (FLOAT f);

    CONST FLOAT    operator *  (CONST MSVector& v) CONST;

    MSVector        operator *  (CONST FLOAT f) CONST;
    MSVector        operator *  (CONST MSMatrix& m) CONST;
    MSVector        operator +  (CONST MSVector& v) CONST;
    MSVector        operator -  (CONST MSVector& v) CONST;

public:
    FLOAT    x_;
    FLOAT    y_;
    FLOAT    z_;
    FLOAT    w_;
};

다음과 같은 포맷을 기준으로 만들 생각이다.

각각 멤버함수들의 설명이나 기본적인 구현에 대한 설명은 생략하겠다.


 
 SIMD를 이용해보겠다는 생각하나만으로 지금까지 달려왔다.

그럼 이 클래스의 구현을 다 SIMD로 해야할까?

그럴 필요가 없다. 오히려 정말 간단한 연산의 경우에는 SIMD를 사용하지 않는게 좋을지도 모르겠다.

모든걸 SIMD로 만든다는 생각을 하는게 아니라

복잡한 연산에 한해서 SIMD를 사용했을 때 속도적 이점이 생길만한 것에 대해서 SIMD를 적용
하는게 옳다.

이번 구현에서는 4가지 함수에 대해서만 SIMD로 구현하고 나머지는 그냥 일반 구현을 이용한다.

1. GetLength()
2. Normalize()
3. Cross()
4. operator * (CONST MSMatrix& m) CONST


하지만 4번의 경우에는 아직 MSMatrix를 정의하지 않았으므로 넘어가고 위의 3가지 함수를 구현해 보겠다.

위의 3개정도 만들어보니까 내 스스로 만들기는 좀 오래걸리겠지만 코드를 읽는대는 어느정도 편해졌다.


1. GetLength()
FLOAT f;

if (isAvailableSSE)
{
    FLOAT* p = &f;

    w_ = 0.0f;

    // MOVUPS    Move unaligned packed single-precision floating-point values
    // MULPS     Multiply packed single-precision floating-point values
    // SHUFPS    Suffle pakced single-precision floating-point values
    // SQURTSS   Compute square root of scalar single-precision floating-point values
    // ADDPS     Add packed single-precision floating-point values
    // MOVSS     Move scalar single-precision floating-point values

    __asm
    {
        mov        ecx,     p                                                            
        mov        esi,     this                                                        
        movups    xmm0,    [esi]                                                        
        mulps     xmm0,    xmm0                                                        
        movaps    xmm1,    xmm0                                                        
        shufps    xmm1,    xmm1,    4Eh                                                    
        addps     xmm0,    xmm1                                                        
        movaps    xmm1,    xmm0                                                        
        shufps    xmm1,    xmm1,    11h                                                    
        addps     xmm0,    xmm1                                                        
        sqrtss    xmm0,    xmm0                                                        
        movss     [ecx],   xmm0                                                        
    }

    w_ = 1.0f;
}
else
{
    f =  sqrtf(x_ * x_ + y_ * y_ + z_ * z_);
}

return f;
이번 구현에서는 주석으로 정리된 6개의 명령어를 새로 배우게 될 것이다. 

명령어들을 알아보기전에 저번에 구현했던 SSE지원 가능 함수를 통해서 지원 가능 여부를 변수로 저장한 뒤

위처럼 지원하면 SSE 명령어를 실행시키고 아니라면 기존 수학함수를 이용한다.

SSE버전 라이브러리, 아닌 버전 라이브러리를 만들고 동적으로 로딩해서 써도 될 것 같기는 한데 일단 이 방식으로 진행한다. 

위에서부터 차근 차근 진행해 나가겠다.
FLOAT f;                              // 결과를 저장할 변수

if (isAvailableSSE)
{
    FLOAT* p = &f;                    // 변수의 주소를 담는 포인터 변수

    w_ = 0.0f;                         // x, y, z값 계산할 때 영향을 끼치지 않기 위해 0.0f으로 설정
 
 
    __asm
    {
        mov        ecx,     p         ; ecx 레지스터에 결과를 저장할 f 변수의 주소를 복사해 놓는다.                                          
        mov        esi,     this      ; esi 레지스터에 현재 인스턴스의 주소를 복사한다.                                                  
        movups    xmm0,    [esi]                                               
movups 명령어는 정렬되지 않은 패킹된 단정도 부동 소수점값을 복사해오는 명령어이다.

xmm0 레지스터에 esi가 가리키고 있는 현재 인스턴스의 값이 복사된다. 
MSVector 클래스의 멤버변수인 x_, y_, z_, w_가 xmm0으로 복사된다는 의미이다.

이때 저장되는 배치가 중요한데 아래 표와 같이 들어가지게 된다.

 128 96  64  32 
 w_ z_  y_  x_ 
       mulps     xmm0,    xmm0                                             
mulps 명령어는 패킹된 단정도 부동 소수점값 곱해주는 명령어이다.

xmm0 레지스터와 xmm0 레지스터를 곱했다는건 같은 값끼리 곱했다는 이야기가 되고 즉 제곱을 했다는 이야기가 된다.

명령어 실행 후 xmm0 레지스터의 값을 표로 확인해 보자. 

 128 96  64  32 
 w_ * w_ z_ * z_ y_ * y_  x_ * x_ 
        movaps    xmm1,    xmm0                                              
movaps 명렁어는 정렬된 패킹된 단정도 부동 소수점 값을 복사해오는 명령어이다.
        shufps    xmm1,    xmm1,    4Eh                                                    
shufps 명령어는 패킹된 단정도 부동 소수점 값을 셔플해주는 명령어이다.

여기서 말한 셔플이란 개념이 좀 생소할 것이다. 셔플 명령어를 이용하면 다양한 연산을 쉽게 할 수 있다.
대신 사용하는방식에 좀 익숙해질 필요는 있다.

이 명령어는 3개의 인자를 받는다.



1) 셔플된 결과를 저장하면서 뒤의 두 인덱스값만큼 셔플 될 레지스터
2) 앞의 두 인덱스값만큼 셔플 될 레지스터
3) 셔플할 인덱스


이렇게 설명을 들어서는 감이 안 올것이다.

먼저 세번째 인자를 생각해보자.

4Eh를 이진수로 풀어적어보면 01001110이 된다.

이 값을 두비트씩 끊어서 보면 01/00/11/10이 된다.
이 값을 xmm 레지스터의 인덱스로 생각을 하고 해당 위치값을 복사해 온다.

현재 xmm1 레지스터의 값을 표로 보면 

 128  96  64  32
 3  2  1  0
 w_ * w_  z_ * z_   y_ * y_  x_ * x_

두번째 행에 위치된 값이 인덱스값이다.

4Eh값을 인덱스값으로 변환시키면 1/0/3/2가 된다.

앞의 1과 0은 두번째 레지스터에서 가져오고 뒤의 3과 2는 첫번째 레지스터에서 가져온다.

여기서는 둘다 xmm1이였기 때문에 xmm1에서 가져오면 된다.

명령어 실행 후 xmm1 레지스터값을 보자. 

 128 96  64  32 
 y_ * y_ x_ * x_ w_ * w_  z_ * z_ 

위치가 인덱스대로 옮겨진 것을 알 수 있다.

  addps xmm0, xmm1

addps 명령어는 패킹된 단정도 부동 소수점 값을 더해주는 명령어이다.

xmm0 레지스터에는 셔플하기전 값이 있고 xmm1 레지스터에는 셔플 후 값이 있다.

명령어 실행 후 xmm0 레지스터값을 보자. 

 128 96  64  32 
w_ * w_ +  y_ * y_ z_  * z_ + x_ * x_ y_ * y_ + w_ * w_  x_ * x_ + z_ * z_
        movaps    xmm1,    xmm0                                                        
        shufps    xmm1,    xmm1,    11h                                                    
        addps     xmm0,    xmm1                                                        
위에서 본 명령어들의 반복이다.

xmm0 레지스터값을 xmm1 레지스터로 다시 옮기고 11h값으로 셔플해준다음 xmm0 레지스터와 더해준다.

셔플부분만 다시 확인해보자.

11h는 00010001로 00/01/00/01 -> 0/1/0/1 로 셔플하라는 이야기이다.

셔플후에 xmm1 레지스터값을 확인해보자.

 128 96  64  32 
x_ * x_ + z_ * z_ y_ * y_ + w_ * w_  x_ * x_ + z_ * z_ y_ * y_ + w_ * w_

xmm0 레지스터와 xmm1 레지스터을 더한 결과를 확인해보자.

 128 96  64  32 
x_ * x_ +  y_ * y_ + z_ * z_ + w_ * w_ x_ * x_ +  y_ * y_ + z_ * z_ + w_ * w_ x_ * x_ +  y_ * y_ + z_ * z_ + w_ * w_ x_ * x_ +  y_ * y_ + z_ * z_ + w_ * w_

길이를 구하는 식중 루트씌우기 전단계까지의 연산이 각각에 들어가게 된다.

이 모습을 연출하기위해 지금까지 셔플하고를 반복하였다.
        sqrtss    xmm0,    xmm0                                                        
        movss     [ecx],   xmm0                                                        
sqrtss 명령어는 단정도 부동 소수점값 하나에 루트를 씌워주는 명령어이다. 첫번째 위치의 값만 변경해준다.

movss 명령어는 단정도 부동 소수점값 하나를 복사해주는 명령어이다.

ecx에 복사해주면 f에 값이 들어가지게 된다.

열심히 셔플하고 더하고 했는데 막상 사용하는건 한개만 사용한다고 하니 약간 허무할 수도 있겠지만 그래도 계산해냈다! 
    w_ = 1.0f;
 w_ 값을 다시 1.0f으로 맞춰준다.

지금까지 벡터의 길이를 구하는 함수를 SIMD로 구현해보았다.

나머지 함수들도 이와 비슷하므로 간략히 정리만 하고 넘어가겠다.



2. Normalize()
w_ = 0.0f;

// RSQRTPS    Compute reciprocals of square roots of packed single-precision floating-point values

__asm
{
    mov        esi,    this                                                                        
    movups    xmm0,    [esi]                                                                        
    movaps    xmm2,    xmm0                                                                        
    mulps     xmm0,    xmm0                                                                        
    movaps    xmm1,    xmm0                                                                        
    shufps    xmm1,    xmm1,    4Eh    
    addps     xmm0,    xmm1        
    movaps    xmm1,    xmm0        
    shufps    xmm1,    xmm1,    11h    
    addps     xmm0,    xmm1        

    rsqrtps   xmm0,    xmm0                                                                        
    mulps     xmm2,    xmm0                                                                        
    movups    [esi],   xmm2                                                                        
}

w_ = 1.0f;

정규화 하는 방법은 벡터를 길이로 나눠주면 된다. 나눈다는 것은 역수를 곱한다는 이야기와 같다.

간단하게 흐름을 정리해보겠다.

xmm0 레지스터에 현재 인스턴스값을 복사해 오고 
xmm2 레지스터에 xmm0 레지스터값을 복사해 놓는다.

위에서 길이를 구한 식과 동일한 방법으로 루트값을 구하기 직전까지 xmm0 레지스터값을 만들어 놓고

rsqrtps 명령어를 사용한다.

rsqrtps 명령어는 패킹된 단정도 부동 소수점 값의 루트를 씌우고 역수를 취한 값을 구해준다.

구한 값을 원래 벡터값을 저장해놓았던 xmm2 레지스터와 곱해줌으로써 구한다.


3. Cross()
__asm
{
    mov        esi,    u
    mov        edi,    v

    movups    xmm0,    [esi]
    movups    xmm1,    [edi]

    movaps    xmm2,    xmm0
    movaps    xmm3,    xmm1

    shufps    xmm0,    xmm0,    0xc9
    shufps    xmm1,    xmm1,    0xd2
    mulps     xmm0,    xmm1

    shufps    xmm2,    xmm2,    0xd2
    shufps    xmm3,    xmm3,    0xc9
    mulps     xmm2,    xmm3

    subps     xmm0,    xmm2

    mov        esi,    this
    movups    [esi],   xmm0
}

w_ = 1.0f;

이번에는 외적을 구해볼 차례다.

이제 코드를 읽는대 큰 문제는 없다고 생각이 된다.

중요한 부분만 보자.

외적 공식을 생각해보면 x값을 구하는 부분은 y와 z로 구성되어 있는 식으로 되어있다.

그런 구현을 위해 셔플을 사용한다.

0xc9 : 11001001 -> 11/00/10/01 -> 3/0/2/1
0xd2 : 11010010 -> 11/01/00/10 -> 3/1/0/2

[w][x][z][y]
[w][y][x][z]


곱해보면 x 위치에는 y*z, y 위치에는 z * x, z 위치에는 x * y, w 위치에는 w 제곱이 들어간다.

그 다음에는 반대로 셔플하여서 반대결과를 만들어내고 셔플된 두 레지스터를 빼줌으로써 외적 계산을 완료한다.


예제를 통해서 SSE 명령을 알아보니까 처음에는 좀 헷갈렸는데 (특히 셔플)

몇개 해보다보니까 어느정도는 익숙해 진 것 같다.

SSE 버전이 높아질 수록 더 격렬한 명령어들이 있었던 것 같은데 그런 명령어들도 하나씩 익숙해져 나아가면 될 것 같다.


SIMD를 이용해서 Vector 클래스를 만들어보았다.

약간 찜찜한점은 SSE만 사용했다는 것인데 SSE2에는 어떤 명령어가 있는지 아직 파악이 안되서 일단 책 대로 따라가겠다.

다음에는 SIMD를 이용해서 Matrix 클래스 만들어 보자.




 
반응형
Posted by msparkms
,