380 likes | 867 Views
최적화 무 료 티켓 : SSE (SIMD Extension). 이권일 EA Seoul Studio (BFO). 발표 대상. C/C++ 프로그래머 H/W 에 관심이 많은 프로그래머 어셈블러는 포함되지 않음. SSE (SIMD Streaming Extension). 1999 년 인텔 펜티엄 3 에 처음 지원 Float Point 및 비교 로직 등 다양한 연산 SSE 전용 128bit XMM 레지스터 8 개 추가 MMX 와 달리 거의 모든 기능이 구현됨. 시작하기.
E N D
최적화 무료 티켓 : SSE (SIMD Extension) 이권일 EA Seoul Studio (BFO)
발표 대상 • C/C++ 프로그래머 • H/W 에 관심이 많은 프로그래머 • 어셈블러는 포함되지 않음
SSE (SIMD Streaming Extension) • 1999년 인텔펜티엄3 에 처음 지원 • Float Point 및 비교 로직 등 다양한 연산 • SSE 전용 128bit XMM 레지스터 8개 추가 • MMX 와 달리 거의 모든 기능이 구현됨
시작하기 #include "stdafx.h“ #include <xmmintrin.h> void _tmain() { size_t count = 16 * 1024 * 1024; // 4 byte * 16M = 64MB // C version float* a = newfloat[count]; float* b = newfloat[count]; for(size_ti=0; i<count;++i) { b[i] = a[i] + a[i]; } // SSE version __m128* a4 = (__m128*) _aligned_malloc(sizeof(float)*count, 16); __m128* b4 = (__m128*) _aligned_malloc(sizeof(float)*count, 16); for(size_ti=0; i<count/4;++i) { b4[i] = _mm_add_ps(a4[i], a4[i]); } }
__m128자료형 typedef union __declspec(intrin_type) _CRT_ALIGN(16) __m128 { float m128_f32[4]; unsigned __int64 m128_u64[2]; __int8 m128_i8[16]; __int16 m128_i16[8]; __int32 m128_i32[4]; __int64 m128_i64[2]; unsigned __int8 m128_u8[16]; unsigned __int16 m128_u16[8]; unsigned __int32 m128_u32[4]; } __m128; • XMM0~XMM7 레지스터나 메모리 상의 데이터를 가르킬 수 있다. -> 지역변수, 할당된 메모리, 클래스 멤버 • 16 바이트 정렬을 요구하기 때문에 _aligned_malloc(), _aligned_free() 등으로 할당 한다. -> 어길 경우 크래쉬 발생 • 정렬 되지 않은 데이터는 __m128_mm_loadu_ps(float*) 함수로 변환이 가능하다.
Single Instruction Multi Data 연산 for(size_ti=0; i<count/4;++i) { b4[i] = _mm_add_ps(a4[i], a4[i]); } • intrinsic 을 사용하여 __m128자료형에 들어있는 데이터들 동시에 조작한다. (첫번째 1개만 조작하는 기능도 지원한다.) • 부등 소숫점 연산, 정수 연산, 비교 연산, 로직 연산, 메모리 접근, 자료형 변환, 캐쉬 제어 지원 • SSE 레지스터만 사용하기 때문에 ALU 와는 독자적으로 작동하게 되어 있다.
편하게 코딩하기 // 산술 연산자 __forceinline__m128operator+(__m128 l, __m128 r) { return_mm_add_ps(l,r); } __forceinline__m128operator-(__m128 l, __m128 r) { return_mm_sub_ps(l,r); } __forceinline__m128operator*(__m128 l, __m128 r) { return_mm_mul_ps(l,r); } __forceinline__m128operator/(__m128 l, __m128 r) { return_mm_div_ps(l,r); } __forceinline__m128operator+(__m128 l, float r) { return_mm_add_ps(l,_mm_set1_ps(r)); } __forceinline__m128operator-(__m128 l, float r) { return_mm_sub_ps(l, _mm_set1_ps(r)); } __forceinline__m128operator*(__m128 l, float r) { return_mm_mul_ps(l, _mm_set1_ps(r)); } __forceinline__m128operator/(__m128 l, float r) { return_mm_div_ps(l, _mm_set1_ps(r)); } // 논리 연산자 __forceinline__m128operator&(__m128 l, __m128 r) { return_mm_and_ps(l,r); } __forceinline__m128operator|(__m128 l, __m128 r) { return_mm_or_ps(l,r); } // 비교 연산자 __forceinline__m128operator<(__m128 l, __m128 r) { return_mm_cmplt_ps(l,r); } __forceinline__m128operator>(__m128 l, __m128 r) { return_mm_cmpgt_ps(l,r); } __forceinline__m128operator<=(__m128 l, __m128 r) { return_mm_cmple_ps(l,r); } __forceinline__m128operator>=(__m128 l, __m128 r) { return_mm_cmpge_ps(l,r); } __forceinline__m128operator!=(__m128 l, __m128 r) { return_mm_cmpneq_ps(l,r); } __forceinline__m128operator==(__m128 l, __m128 r) { return_mm_cmpeq_ps(l,r); }
SIMD 정말 4배 빠른가요? // C 버젼 for(size_ti=0; i<count;++i) { b[i] = a[i] + a[i]; } -> 실행 시간 49.267 ms // Compiler Intrinsic 버젼 for(size_ti=0; i<count/4;++i) { b4[i] = a4[i] + a4[i]; } -> 실행 시간 47.927 ms
메모리 병목 현상 !!! • C49.267 ms VS SSE 47.927 ms • 캐쉬 최적화를 하거나… • 메모리 접근을 줄이거나… • 다른 일을 더 시키거나…
연산량을 늘리자! sinf() // sin(a) = a – (a^3)/3! + (a^5)/5! – (a^7)/7! … float req_3f = 1.0f / (3.0*2.0*1.0); float req_5f = 1.0f / (5.0*4.0*3.0*2.0*1.0); float req_7f = 1.0f / (7.0*6.0*5.0*4.0*3.0*2.0*1.0); for(size_ti=0; i<count; ++i) { b[i] = a[i] - a[i]*a[i]*a[i]*req_3f + a[i]*a[i]*a[i]*a[i]*a[i]*req_5f - a[i]*a[i]*a[i]*a[i]*a[i]*a[i]*a[i]*req_7f; } -> 실행 시간 111. ms
SSE 버젼의 sinf() // sin(a) = a – (a^3)/3! + (a^5)/5! – (a^7)/7! … __m128 req_3f4 = _mm_set1_ps(req_3f); __m128 req_5f4 = _mm_set1_ps(req_5f); __m128 req_7f4 = _mm_set1_ps(req_7f); for(size_ti=0; i<count/4; ++i) { b4[i] = a4[i] - a4[i]*a4[i]*a4[i]*req_3f4 + a4[i]*a4[i]*a4[i]*a4[i]*a4[i]*req_5f4 - a4[i]*a4[i]*a4[i]*a4[i]*a4[i]*a4[i]*a4[i]*req_7f4; } -> 실행 시간 48.939 ms
a+a과 sin() 연산 시간이 같다 ? • C 에서 a[i] + b[i] 를 구성하는데 2.5 명령어로 실행되었고 sin() 은 19.5 명령어로 실행 (Loop Unrolling) • SSE 에서 a4[i] + b4[i] 를 구성하는데 6 명령어로 실행되었고 sin() 은 29 명령어로 실행
Out of Order • CPU 내부에 들어온 명령어를 순서에 관계 없이 실행 • Core 아키텍쳐의 경우 클럭당 최대 4개의 명령까지 순차 실행 가능 • 메모리 접근 시간을 활용하여 읽고 쓰는 중에 데이터 처리
그럼 왜 C 코드는 느려요? • SSE 는 SIMD 명령으로 4배의 연산 • C 언어에 SSE 컴파일 옵션사용시 빨라진다. • FPU 는 구조적인 문제로 SSE 유닛보다 느리다.
_mm_stream_ps()를 씁시다!! // C 버젼 for(size_ti=0; i<count;++i) { b[i] = a[i] + a[i]; } -> 실행 시간 49.267 ms // a+a stream 버젼 for(size_ti=0; i<count/4;++i) { _mm_stream_ps((float*)(b4+i), _mm_add_ps(a4[i], a4[i])); } -> 실행 시간 30.114 ms
_mm_stream_ps() 는 빠르다 !! • Move Aligned Four Packed Single-FP Non Temporal • CPU 캐쉬를 거치지 않고 메모리에 데이터를 전송한다. • 쓰기 순서를 보장하지 않으므로 쓰고 바로 읽으면 안됨
그렇다면 sin() 도 빨라질까 ? // SSE intrinsic for(size_ti=0; i<count/4; ++i) { b4[i] = a4[i] - a4[i]*a4[i]*a4[i]*req_3f4 + a4[i]*a4[i]*a4[i]*a4[i]*a4[i]*req_5f4 - a4[i]*a4[i]*a4[i]*a4[i]*a4[i]*a4[i]*a4[i]*req_7f4; } -> 실행 시간 48.939 ms // SSE intrinsic + _mm_stream_ps() for(size_ti=0; i<count/4; ++i) { _mm_stream_ps( (float*)(b4+i), a4[i] - a4[i]*a4[i]*a4[i]*req_3f4 + a4[i]*a4[i]*a4[i]*a4[i]*a4[i]*req_5f4 - a4[i]*a4[i]*a4[i]*a4[i]*a4[i]*a4[i]*a4[i]*req_7f4 ); } -> 실행 시간 32.081 ms
컴파일러를 좋아하는가? • 컴파일러는 멋지지만 완벽하지 않다. • 컴파일러는 최적화를 도와줄 뿐이다. • intirinsic은 기본적인 최적화만 지원한다. • ASM Listing 에서 최적화를 확인하자.
더 많은 복잡한 일이 가능하다 !! • float Read + Write 시간 : 2.896 ns • __m128 Read + Write 시간 : 11.214 ns • __m128 Read + Stream 시간 : 6.977 ns
좀더 실용적인 예제 : Skinning • Vertex : 1024 * 1024 • Bone : 200 • 4 weight per vertex + normal + tangent • SSE 컴파일 옵션이 켜진 C, SSE최적화 • 스키닝 없는 C 루프 복사, SSE 루프 복사, memcpy()
C Skinning Code // Optimized C Version D3DXMATRIX m = b[in->index[0]] * in->blend[0] + b[in->index[1]] * in->blend[1] + b[in->index[2]] * in->blend[2] + b[in->index[3]] * in->blend[3]; out->position.x = in->position.x*m._11 + in->position.y*m._21 + in->position.z*m._31 + m._41; out->position.y = in->position.x*m._12 + in->position.y*m._22 + in->position.z*m._32 + m._42; out->position.z = in->position.x*m._13 + in->position.y*m._23 + in->position.z*m._33 + m._43; out->normal.x = in->normal.x*m._11 + in->normal.y*m._21 + in->normal.z*m._31; out->normal.y = in->normal.x*m._12 + in->normal.y*m._22 + in->normal.z*m._32; out->normal.z = in->normal.x*m._13 + in->normal.y*m._23 + in->normal.z*m._33; out->tangent.x = in->tangent.x*m._11 + in->tangent.y*m._21 + in->tangent.z*m._31; out->tangent.y = in->tangent.x*m._12 + in->tangent.y*m._22 + in->tangent.z*m._32; out->tangent.z = in->tangent.x*m._13 + in->tangent.y*m._23 + in->tangent.z*m._33;
SSE Skinning Code // SSE Code __m128 b0 = _mm_set_ps1(in->blend[0]); __m128 b1 = _mm_set_ps1(in->blend[1]); __m128 b2 = _mm_set_ps1(in->blend[2]); __m128 b3 = _mm_set_ps1(in->blend[3]); __m128* m[4] = { (__m128*)( matrix+in->index[0] ), (__m128*)( matrix+in->index[1] ), (__m128*)( matrix+in->index[2] ), (__m128*)( matrix+in->index[3] ) }; __m128 m0 = m[0][0]*b0 + m[1][0]*b1 + m[2][0]*b2 + m[3][0]*b3; __m128 m1 = m[0][1]*b0 + m[1][1]*b1 + m[2][1]*b2 + m[3][1]*b3; __m128 m2 = m[0][2]*b0 + m[1][2]*b1 + m[2][2]*b2 + m[3][2]*b3; __m128 m3 = m[0][3]*b0 + m[1][3]*b1 + m[2][3]*b2 + m[3][3]*b3; _mm_stream_ps( out->position, m0*in->position.x+m1*in->position.y+m2*in->position.z+m3 ); _mm_stream_ps( out->normal, m0*in->normal.x+m1*in->normal.y+m2*in->normal.z ); _mm_stream_ps( out->tangent, m0*in->tangent.x+m1*in->tangent.y+m2*in->tangent.z );
SSE Skinning 결과 • memcpy() 시간의 80% 로 스키닝을 할 수 있다. • 파티클, UI 등에 유용하게 사용할 수있다. • Dynamic VB 를 쓰는 동안 계산을 추가로 할 수 있다.
Masking 예제 : KdTree • Ray-Trace 에 특화된 Binary Tree (Axis Aligned BSP) • Deep-Narrow Tree 를 만들어야 효율이 좋아지므로 노드가 무척 많아진다. • Tree Node 방문이 전체 처리 시간의 90% 을 차지한다.
KdTree코드 // KdTree SSE Version for(int y=0; y<g_height; y+=2) { for(int x=0; x<g_width; x+=2) { __m128orig[3] = { _mm_set_ps(x+0.0f,x+1.0f,x+0.0f,x+1.0f), _mm_set_ps(y+0.0f,y+0.0f,y+1.0f,y+1.0f), _mm_set1_ps(-500.0f), }; __m128 dir[3] = { _mm_set1_ps(0), _mm_set1_ps(0), _mm_set1_ps(1), }; TKdTree<TTriangle>::HitResult4 result; // g_mask4 는 { FFFFFFFF, FFFFFFFF, FFFFFFFF, FFFFFFFF } __m128hitMask = g_kdTree.HitTest4(g_mask4, orig, dir, &result); } }
SSE 코딩을 시작할때는.. • 알고리즘 구현과 디버깅은 C 언어로 먼저 마친다. • 같은 알고리즘으로 동일한 결과를 유지한다 -> assert( TestC() == TestSSE() ); • 같은 데이터로 동일한 결과를 여러번 돌려 볼 수 있는 테스트 환경을 만든다.