반응형


이번엔 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
,
반응형


요즘 CPU 중에 SSE 지원 안되는게 거의 없을 것 같기는 하지만

CPU의 정보를 얻어와서 어떤 SIMD를 지원하는지 알아보기로 하자.


 
CPU 정보를 얻어오는 명령어는 CPUID 이다.

EAX값을 어떤 값으로 설정하느냐에 따라서 CPUID를 실행했을 때의 결과값이 바뀐다.

간단히 정리하면 아래와 같다.

EAX = 0 : 제조업체 ID 가져오기
EAX = 1 : 프로세서 정보 및 기능 비트
EAX = 2 : 캐시 및 TLB 서술자 정보
EAX = 3 : 프로세서 일련 번호
EAX = 80000000h : 가장 높은 확장 함수 가져오기
EAX = 80000001h : 확장 프로세서 정보 및 기능 비트
EAX = 80000002h,80000003h,80000004h         : 프로세서 브랜드 문자열
EAX = 80000005h : 1차 캐시 및 TLB ID
EAX = 80000006h : 확장 2차 캐시 기능
EAX = 80000007h : 고급 전원 관리 정보
EAX = 80000008h : 가상 및 물리 주소 크기

우리가 체크해보려고 하는 것은 다음과 같다.

1. 생산자 이름
2. SSE 지원 여부
3. SSE2 지원 여부
4. MMX 지원 여부
5. 3DNow 지원 여부
6. 확장 기능을 지원하는가?
7. MMX 확장 지원 여부
8. 3DNow 확장 지원 여부


struct SMSCPUInfo
{
    bool    sse_;         // Streaming SIMD Extension
    bool    sse2_;        // Streaming SIMD Extension 2
    bool    threeNow_;    // 3DNow!
    bool    mmx_;         // MMX
    
    bool    ext_;         // 확장된 기능이 사용가능한가?
    bool    mmxExt_;      // MMX 
    bool    threeNowExt_; // 3DNow!
    
    char    vendor_[13];    // 생산자 이름
};

다음과 같은 구조체가 있다고 생각하고 이후 진행을 하겠다.

하나씩 해보자.


 
1. 생산자 이름

위의 설명에서 보면 나오듯이 EAX에 0을 넣고 CPUID를 호출해 주면 나온다.

생산자 이름은 12바이트로 3개의 레지스터에 걸쳐서 나온다.

순서대로 적으면 ebx, edx, ecx 순으로 저장해야 알맞게 나온다.

CHAR* vendorName = cpuInfo.vendor_;
mov        eax,                    0            // 생산자 이름을 얻을 수 있다.    
CPUID                                

mov        esi,                    vendorName   // 포인터 연결     
mov        [esi],                  ebx          // 처음 4바이트  
mov        [esi + 4],              edx          // 두번째 4바이트
mov        [esi + 8],              ecx          // 세번째 4바이트


2. SSE 지원여부
3. SSE2 지원여부
4. MMX 지원여부


3개를 같이 하는 이유는 같은 방식으로 구하기 때문이다.

EAX에 1을 넣고 CPUID를 호출해 주면 edx에 값이 변경되고 이 값을 가지고 비교해서 각각 지원하는지 알 수 있다. 
mov        eax,                    1           // 지원 기능들을 얻어온다.
CPUID

test       edx,                    04000000h   // SSE2를 지원하는가?
jz        __NOSSE2                             // 지원하지 않는다면 __NOSSE2로 넘어간다.
mov        [cpuInfo.sse2_],        1           // 지원하면 sse2_값을 1로 변경한다.

__NOSSE2:
test       edx,                    02000000h   // SSE를 지원하는가?
jz        __NOSSE                              // 지원하지 않는다면 __NOSSE로 넘어간다.
mov        [cpuInfo.sse_],         1           // 지원하면 sse_값을 1로 변경한다.
__NOSSE:
test       edx,                    00800000h   // MMX를 지원하는가?
jz        __NOMMX                              // 지원하지 않느다면 __NOMMX로 넘어간다.
mov        [cpuInfo.mmx_],         1           // 지원하면 mmx_값을 1로 변경한다.
__NOMMX:
위의 소스대로 edx 값과 04000000h 와 같은 특정 비트 값들을 &연산으로 비교해봐서

1이면 지원하는거고 0이면 지원하지 않는것으로 판단한다.


* 예외처리  
__try
{
    __asm
    {
        mov        eax,            0                
    }
}
_except(EXCEPTION_EXECUTE_HANDLER)                          // 실행중 예외가 발생했다면                  
{
    if (STATUS_ILLEGAL_INSTRUCTION ==_exception_code())     // 예외 코드가 합법적이지않은 명령어
    {                                                       // 였다면 처리.. 여기서는 리턴
        return cpuInfo;                                    
    }
}    


5. 3DNow 지원 여부
6. 확장 기능을 지원하는가?


이 처리도 위와 다를것은 없지만 eax값이 좀 다르므로 따로 설명하겠다.

eax에 80000000h를 넣고 CPUID를 호출해주면 확장 함수와 관련된 내용을 얻어올 수 있다.
eax에 80000001h를 넣고 CPUID를 호출해주면 확장 프로세서 정보를 얻어올 수 있다.

이를 이용해 위의 두가지 정보를 얻어오자.  
__asm
{
    mov        eax,                     80000000h         // 확장 함수 정보를 얻어온다.
    CPUID

    cmp        eax,                     80000000h         // eax값과 80000000h값의 차이를 구한다.
    jbe        __NOEXT                                    // 작거나 같으면 __NOEXT로 넘어간다.
    mov        [cpuInfo.ext_],          1                 // 아니라면 ext_를 1로 변경한다.

    mov        eax,                     80000001h         // 확장 프로세서 정보를 얻어온다.
    CPUID

    test       edx,                     80000000h         // 지원 정보가 있는가?
    jz        __NOEXT                                     // 없다면 __NOEXT로 넘어간다.
    mov        [cpuInfo.threeNow_],     1                 // 있다면 threeNow_를 1로 변경한다.
__NOEXT:

7. MMX 확장 지원 여부
8. 3DNow 확장 지원 여부


AMD 프로세서에 해당하는 이야기이므로 AMD인지 구분해야 한다.

if (0 == strncmp(cpuInfo.vendor_, "AuthenticAMD", 12) && cpuInfo.ext_) // AMD CPU인가?

mov        eax,                    0x80000001          // 확장 함수 정보를 얻어온다.  
CPUID

test       edx,                     0x40000000         // 3DNow 확장을 지원하는가?
jz         __NOEXT3DNOW                                // 지원하지 않으면 __NOEXT3DNOW로 넘어간다.
mov        [cpuInfo.threeNowExt_], 1                   // 지원하면 threeNowExt_를 1로 변경한다.
__NOEXT3DNOW:
test       edx,                     0x00400000         // MMX 확장을 지원하는가?
jz         __NOEXTMMX                                  // 지원하지 않으면 __NOEXTMMX로 넘어간다.  
mov        [cpuInfo.mmxExt_],      1                   // 지원하면 mmxExt_를 1로 변경한다.
__NOEXTMMX:


여기까지가 CPU정보를 통해서 SSE등이 지원되는지 알아보았다.

하지만 한가지 더 체크해야 할 것이있다.

바로 OS이다.

CPU가 지원되지만 OS에서 명령어셋을 지원하지 않을 수도 있다.
__try
{
    __asm xorps    xmm0, xmm0                            // SSE 명령어 한번 쳐리해보고                           
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
    if (STATUS_ILLEGAL_INSTRUCTION == _exception_code()) // 지원되지 않는다는 예외가 오면 지원 안함   
        return false;

    return true;                                        // 아니면 지원함  
}
 상당히 쿨한 방식으로 판단할 수 있다.


 
CPU와 OS에서 지원하면 SSE를 지원한다고 판단하고 IDE에 SSE 명령까지 쓴다고 설정만 해놓고 코딩하면된다.

이것을 판단하는 이유는 수학 라이브러리가 SSE가 되는 곳에서만 돌아가면 안되기 때문에 지원 여부를 bool값으로 두고

그 값을 통해서 SSE 명령어를 쓸것인가 아니면 일반 연산자를 쓸 것인가 판단하기 위함이다.

이제 본격적으로 SSE를 통해서 수학 라이브러리를 하나씩 만들어 보자.


 
반응형
Posted by msparkms
,
반응형



엔진 프로그래밍 공부를 하다가 SIMD 관련된 파트가 나와서 어셈블리란에 정리해보려고 한다.

목표는 SIMD를 이용해서 수학 라이브러리를 만드는 것이다.

이번에 배울 목표이다.

1. 최적화
2. SIMD란?
3. SIMD의 간단한 역사
4. 마무리


 
1. 최적화

SIMD란? 이란 제목으로 글을 올려놓고 갑자기 최적화부터 이야기가 나오니 황당할 수도 있다.
일단 SIMD에 대해서 알아보기 전에 최적화에 대해서 개념을 잘 잡아놔야 된다고 생각이 들었고 책 순서 또한 그렇다. 
SIMD가 뭔지는 몰라도 어셈블리란에 글이 올라오고 있고 이를 이용해 수학 라이브러리를 만든다고 하니
SIMD를 이용하면 보통보다 연산이 빠른 수학 라이브러리를 만들 수 있을 거라고 예상할 수 있을 것이다.
그렇다. SIMD를 이용하면 평범하게 프로그래밍을 했을 때 보다 더 빠른 연산을 할 수 있다. 

즉 최적화에 SIMD를 이용하면 성능개선을 할 수 있다는 것이다.
하지만 모든 소스를 SIMD로 만들수는 없기 때문에...
최적화를 언제 어디서 해야하는지를 먼저 알아야한다.

최적화를 하려고 할 때 먼저 해봐야 할 것은 프로파일링이다.
프로그램이 실행중일 때 어느 부분이 가장 자주 실행이 되는지 어느 부분에서 병목현상이 일어나는지를 파악해야 한다.
그 지점부터 조금씩 최적화해 나가면서 테스트해보고 다음 병목현상을 찾고 수정하고를 반복한다.

최적화를 할 때 생각해봐야 할 것이 있는데 프로그래밍적인 트릭이나 데이터 타입을 현란하게 사용하거나 이상한 어셈블리 코드를 짠다거나 이런 시도는 안하는 것이 좋다. 차라리 로직에서 사용한 자료구조나 알고리즘부분을 확인하고 문제점은 없는지 더 빠른 알고리즘은 없는지 확인하고 적용하는것이 이후 처리에 도움이 된다.

가슴 아픈 이야기지만 요즘 컴파일러들이 많이 발전해서 어정쩡하게 어셈블리 코드로 최적화 하는 것 보다 컴파일러들이 더 CPU 친화적으로 최적화를 해준다고 한다. 직접 최적화를 할 것이면 이와 비교해 보면서 해보면 될 것 같다.;;

이 다음에는 책 순서상 어셈블리의 기본에 대해서 나오는데 이는 생략하도록 하겠다.
별 내용이 없기 때문이다.



2. SIMD란?

SIMDSingle Instruction Multiple Data의 약자로 이름 그대로의 일을 해준다.
하나의 명령으로 여러개의 데이터를 처리한다는 이야기이다.

기존의 처리방식은 순차적이였기 때문에 복잡한 연산을 할 때 속도를 올리는 방법은 
CPU의 클럭 속도를 높이는 정도였다.

하지만 SIMD를 사용하면 하나의 명령으로 여러개의 데이터를 처리.. 즉, 병렬로 처리가 가능하다는 이야기다.

예를 들어보자.

3차원 벡터의 합을 구하는 연산을 해야 한다고 했을 때 기존 방식은 순차적으로 이루어 질 것이다.

1) 각각의 x값들을 레지스터에 옮긴다. (32bits)
2) 더하기 명령을 실행한다.
3) 각각의 y값들을 레지스터에 옮긴다. (32bits)
4) 더하기 명령을 실행한다.
5) 각각의 z값들을 레지스터에 옮긴다. (32bits)
6) 더하기 명령을 실행한다.


데이터들의 오고 감을 간략화 해도 최소 x, y, z에 대해서 따로 처리를 했어야 했다.

SIMD를 사용했을 때의 방식을 보자.

1) x, y, z를 배열로 만들어 레지스터에 옮긴다. (96bits)
2) 더하기 명령을 실행한다.


이게 끝이다. 대신 앞에 레지스터로 옮기는 과정에서 배열을 이용하는 데이터 패킹 과정이 있지만
한번의 명령으로 동시에 처리됨을 알 수 있다.

기존 레지스터 크기보다 큰 레지스터에 값들을 패킹하여 담고 한번의 연산을 통해 여러개의 결과를 얻어내는 것.
이것이 SIMD의 기본적인 내용이다.


 
3. SIMD의 간단한 역사

SIMD 이야기를 듣다보면 MMX, 3DNow!, SSE 막 이런 이야기들이 나온다.

처음 들어보면 SSE라는 거에 SIMD가 들어있는건가? SIMD가 SSE라는 건가? 헷갈릴 수도 있을 것이다. (내가 그랬다..) 

이번 기회로 SIMD가 어떤식으로 발전해 왔는지 간단하게 집고 넘어가보자.

SIMD라는 개념이 나왔고 이를 각각의 CPU 회사에서 자신의 CPU에 적용하기 시작했다. 

그래서 먼저 나온 것이 MMX이다.

1) MMX

MMXMultimedia Extensions의 약자로 이름 그대로 멀티미디어 처리를 위해서 나온 확장 명령어이다.

MMX도 SIMD의 기본개념을 구현하기 위해 기존의 32bits보다 큰 64bits짜리 레지스터를 이용했다.
32bits짜리를 동시에 2개를 계산할 수 있었다. 1개 계산하는 것보다야 동시에 2개씩 하니까 더 좋다.

근데 가슴이 아팠던 사실이 있는게.. 이 레지스터로 정수형 처리를 했다!!
3D 실시간 게임에서는 실수형 연산이 필수적인데 정수형이라니..

심지어 이론상은 64bits 레지스터를 이용한다라고 했지만 MMX가 나올 당시의 Intel은 64bits짜리 레지스터를 넣지못했고
기존 FPU 스택에서 쓰던 80bits짜리 레지스터를 공유하여 사용하여 정수형 연산을 하였다.

2) 3DNow!

Intel에서 먼저 냈으니 AMD도 질 수 없다. 역시 80bits짜리 실수형 레지스터를 공유하되 실수형 연산이 되게 만들었다.
근데 정수형 연산은 안되었다. 그래도 float을 바로 넣을 수 있다는게 어디인가?
다음버전 3DNow에는 정수형 연산까지 되게끔 만들었다고 한다.

3DNow Professional인가까지 더 나왔었는데 현재 나오는 CPU에는 3DNow를 다 빼버리고 SSE쪽이 지원되게 한 모양이다.

3) SSE

MMX와 3DNow을 지나오면서 Intel이 다음 SIMD 명령어를 만들어내었다. 그것이 바로 SSE이다.
SSEStreaming SIMD Extensions의 줄임말로 현재 많이 사용되는 SIMD 명령어이다.
책에서도 SSE 계열을 기준으로 설명해준다.

게임에서는 3D지만 계산의 편의성등에 의해서 4개까지 계산에 들어가곤한다.
이를 고려한것인지는 모르겠지만 SSE는 128bits까지 지원하는 새로운 레지스터를 추가해버려서 3DNow를 보내버렸다.
128bits면 float 4개까지 커버가 가능하니까 딱 우리가 원하는 사이즈라 할 수 있다.

여기가 펜티엄 3이다.

4) SSE2

펜티엄 4에서 처음 지원된 SIMD 명령어로 SSE에 캐시 제어등 기타 제어 명령이 추가되었다고 한다.

5) SSE3

기존까지 수직적인 계산 명령어만 지원되었다면 SSE3부터는 어느 부분은 더하고 어느 부분은 빼고 하는 수평적인 명령어나 연산 조합 순서가 다양한 명령어들이 제공되었다.

6) SSSE3

SSE3의 개정판?? 정도로 오버플로우시 보정 옵션이 들어간 것이 가장 큰 특징이라고 한다.

7) SSE4

내적등 수학적인 연산 명령이 추가되었다고 한다.

8) SSE5

곱하면서 더해버리기 명령등이 추가되어서 내적구할 때나 행렬곱셈등등 수학적 연산에 속도 향상이 될만한 명령어들이 추가되었다고 함.

9) AVX

Advanced Vector eXteionsions의 줄임말로 256bits짜리 레지스터로 바꿔버린 녀석.. SSE5와 같은 수학적 연산에 도움이 되는 명령어들 까지 지원된다고 하니 다양한 활용이 가능할 것 같아보인다.



4. 마무리 

아직 해보지는 않았지만 Visual Studio 2010에서 SSE2까지 지원한다고 찾아보니 나왔다.

다시 찾아보긴 해야 할듯...

다음부터는 SSE기준으로 하나씩 정리해보자.


 
반응형
Posted by msparkms
,
반응형
어셈블리어를 다시 공부하려고 한다.

처음부터 정리도 해보겠지만 일단 지금 당장 사용할 수 있는 간단한 내용이 있어서 정리한다.

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

소프트웨어 렌더링을 하다가 화면을 배경색으로 덮어주는 Clear() 함수를 구현하면서

4Byte의 값으로 배열을 채워주는 함수가 필요했다.

빠른 속도를 위해 memset()을 이용하려고 했으나 memset()은 1Byte 기준으로 처리가 되기 때문에

4Byte 색상을 적용하는데 좋지 못했다.

그래서 4Byte를 적용할 수 있는 MEMSET을 구현하는 방법을 찾아서 적용하였다.

소스는 다음과 같다.

inline void MEMSET_DWORD(void* dest, const DWORD data, const unsigned int count)
{
 __asm
 {
  mov edi, dest
  mov ecx, count
  mov eax, data
  rep stosd
 }
}

위의 mov 명령어를 통한 레지스터 셋팅은 rep stosd 호출을 위한 값 셋팅이다.

rep - ecx

stosd - eax, edi


이런식으로 연결된다고 보면 된다.

rep -> ecx값 만큼 반복적으로 명령을 수행
stosd -> eax에 담긴 값을 edi에 복사함


stosd의 경우에는 이런 계통 명령어가 그렇듯 stosb(1Byte), stosw(2Bytes), stosd(4Bytes)명령어중
4Bytes를 복사하는 명령어이다.

stosd명령어가 edi가 가리키는 곳에 eax에 담긴 4Bytes값을 복사하면 edi는 그 뒷 주소를 가리키게 되고
rep명령어에 의해 ecx 즉 count값이 0이 될때까지 이를 반복하기 때문에 memset과 같은 기능을 하게 된다.

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

아직 SIMD등 병렬적인 처리가 되는 부분은 익숙하지 않아서

적용해 보지 못했지만 어셈블리어를 다시 공부하면서 적용해 보려고 한다.
반응형
Posted by msparkms
,