1 / 36

최적화 무 료 티켓 : SSE (SIMD Extension)

최적화 무 료 티켓 : SSE (SIMD Extension). 이권일 EA Seoul Studio (BFO). 발표 대상. C/C++ 프로그래머 H/W 에 관심이 많은 프로그래머 어셈블러는 포함되지 않음. SSE (SIMD Streaming Extension). 1999 년 인텔 펜티엄 3 에 처음 지원 Float Point 및 비교 로직 등 다양한 연산 SSE 전용 128bit XMM 레지스터 8 개 추가 MMX 와 달리 거의 모든 기능이 구현됨. 시작하기.

gerik
Download Presentation

최적화 무 료 티켓 : SSE (SIMD Extension)

An Image/Link below is provided (as is) to download presentation Download Policy: Content on the Website is provided to you AS IS for your information and personal use and may not be sold / licensed / shared on other websites without getting consent from its author. Content is provided to you AS IS for your information and personal use only. Download presentation by click this link. While downloading, if for some reason you are not able to download a presentation, the publisher may have deleted the file from their server. During download, if you can't get a presentation, the file might be deleted by the publisher.

E N D

Presentation Transcript


  1. 최적화 무료 티켓 : SSE (SIMD Extension) 이권일 EA Seoul Studio (BFO)

  2. 발표 대상 • C/C++ 프로그래머 • H/W 에 관심이 많은 프로그래머 • 어셈블러는 포함되지 않음

  3. SSE (SIMD Streaming Extension) • 1999년 인텔펜티엄3 에 처음 지원 • Float Point 및 비교 로직 등 다양한 연산 • SSE 전용 128bit XMM 레지스터 8개 추가 • MMX 와 달리 거의 모든 기능이 구현됨

  4. 시작하기 #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]); } }

  5. __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*) 함수로 변환이 가능하다.

  6. 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 와는 독자적으로 작동하게 되어 있다.

  7. 편하게 코딩하기 // 산술 연산자 __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); }

  8. 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

  9. 메모리 병목 현상 !!! • C49.267 ms VS SSE 47.927 ms • 캐쉬 최적화를 하거나… • 메모리 접근을 줄이거나… • 다른 일을 더 시키거나…

  10. 연산량을 늘리자! 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

  11. 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

  12. a+a과 sin() 연산 시간이 같다 ? • C 에서 a[i] + b[i] 를 구성하는데 2.5 명령어로 실행되었고 sin() 은 19.5 명령어로 실행 (Loop Unrolling) • SSE 에서 a4[i] + b4[i] 를 구성하는데 6 명령어로 실행되었고 sin() 은 29 명령어로 실행

  13. Out of Order • CPU 내부에 들어온 명령어를 순서에 관계 없이 실행 • Core 아키텍쳐의 경우 클럭당 최대 4개의 명령까지 순차 실행 가능 • 메모리 접근 시간을 활용하여 읽고 쓰는 중에 데이터 처리

  14. 그럼 왜 C 코드는 느려요? • SSE 는 SIMD 명령으로 4배의 연산 • C 언어에 SSE 컴파일 옵션사용시 빨라진다. • FPU 는 구조적인 문제로 SSE 유닛보다 느리다.

  15. 더 복잡한 계산을 걸어봅시다!

  16. 몇배나 빠르다고요?

  17. _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

  18. _mm_stream_ps() 는 빠르다 !! • Move Aligned Four Packed Single-FP Non Temporal • CPU 캐쉬를 거치지 않고 메모리에 데이터를 전송한다. • 쓰기 순서를 보장하지 않으므로 쓰고 바로 읽으면 안됨

  19. 그렇다면 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

  20. Stream 을 추가한 그래프 !!

  21. 4배 ??

  22. 컴파일러를 좋아하는가? • 컴파일러는 멋지지만 완벽하지 않다. • 컴파일러는 최적화를 도와줄 뿐이다. • intirinsic은 기본적인 최적화만 지원한다. • ASM Listing 에서 최적화를 확인하자.

  23. 어셈블러 최고 !!

  24. 어셈블러최고 !!!!

  25. 더 많은 복잡한 일이 가능하다 !! • float Read + Write 시간 : 2.896 ns • __m128 Read + Write 시간 : 11.214 ns • __m128 Read + Stream 시간 : 6.977 ns

  26. 좀더 실용적인 예제 : Skinning • Vertex : 1024 * 1024 • Bone : 200 • 4 weight per vertex + normal + tangent • SSE 컴파일 옵션이 켜진 C, SSE최적화 • 스키닝 없는 C 루프 복사, SSE 루프 복사, memcpy()

  27. 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;

  28. 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 );

  29. SSE Skinning 결과 • memcpy() 시간의 80% 로 스키닝을 할 수 있다. • 파티클, UI 등에 유용하게 사용할 수있다. • Dynamic VB 를 쓰는 동안 계산을 추가로 할 수 있다.

  30. KdTree

  31. Masking 예제 : KdTree • Ray-Trace 에 특화된 Binary Tree (Axis Aligned BSP) • Deep-Narrow Tree 를 만들어야 효율이 좋아지므로 노드가 무척 많아진다. • Tree Node 방문이 전체 처리 시간의 90% 을 차지한다.

  32. kDTree Traverse

  33. kDTree Packet Traverse

  34. 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); } }

  35. SSE 코딩을 시작할때는.. • 알고리즘 구현과 디버깅은 C 언어로 먼저 마친다. • 같은 알고리즘으로 동일한 결과를 유지한다 -> assert( TestC() == TestSSE() ); • 같은 데이터로 동일한 결과를 여러번 돌려 볼 수 있는 테스트 환경을 만든다.

  36. ?

More Related