반응형
이번엔 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;
명령어들을 알아보기전에 저번에 구현했던 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, xmm0mulps 명령어는 패킹된 단정도 부동 소수점값 곱해주는 명령어이다.
xmm0 레지스터와 xmm0 레지스터를 곱했다는건 같은 값끼리 곱했다는 이야기가 되고 즉 제곱을 했다는 이야기가 된다.
명령어 실행 후 xmm0 레지스터의 값을 표로 확인해 보자.
128 | 96 | 64 | 32 |
w_ * w_ | z_ * z_ | y_ * y_ | x_ * x_ |
movaps xmm1, xmm0movaps 명렁어는 정렬된 패킹된 단정도 부동 소수점 값을 복사해오는 명령어이다.
shufps xmm1, xmm1, 4Ehshufps 명령어는 패킹된 단정도 부동 소수점 값을 셔플해주는 명령어이다.
여기서 말한 셔플이란 개념이 좀 생소할 것이다. 셔플 명령어를 이용하면 다양한 연산을 쉽게 할 수 있다.
대신 사용하는방식에 좀 익숙해질 필요는 있다.
이 명령어는 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 레지스터값을 보자.
위치가 인덱스대로 옮겨진 것을 알 수 있다.
addps xmm0, xmm1
addps 명령어는 패킹된 단정도 부동 소수점 값을 더해주는 명령어이다.
xmm0 레지스터에는 셔플하기전 값이 있고 xmm1 레지스터에는 셔플 후 값이 있다.
명령어 실행 후 xmm0 레지스터값을 보자.
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], xmm0sqrtss 명령어는 단정도 부동 소수점값 하나에 루트를 씌워주는 명령어이다. 첫번째 위치의 값만 변경해준다.
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 버전이 높아질 수록 더 격렬한 명령어들이 있었던 것 같은데 그런 명령어들도 하나씩 익숙해져 나아가면 될 것 같다.
약간 찜찜한점은 SSE만 사용했다는 것인데 SSE2에는 어떤 명령어가 있는지 아직 파악이 안되서 일단 책 대로 따라가겠다.
다음에는 SIMD를 이용해서 Matrix 클래스 만들어 보자.
반응형
'프로그래밍 > 어셈블리어' 카테고리의 다른 글
SIMD를 이용한 수학 라이브러리 만들기 - 2. CPU 식별하기 (0) | 2011.11.10 |
---|---|
SIMD를 이용한 수학 라이브러리 만들기 - 1. SIMD란? (0) | 2011.11.07 |
4Bytes Memset 구현 (0) | 2011.07.06 |