Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

이권일 Sse 를 이용한 최적화와 실제 사용 예

12,001 views

Published on

Published in: Technology, Business
  • Be the first to comment

이권일 Sse 를 이용한 최적화와 실제 사용 예

  1. 1. SSE 를 이용한 최적화와 실제 사용 예<br />이권일<br />EA Seoul Studio (BFO)<br />
  2. 2. 발표 대상<br />C/C++ 프로그래머<br />H/W 및 최적화에 관심 있는 자<br />GPGPU 를 준비하는 자<br />
  3. 3. SSE (SIMD Streaming Extension)<br />1999년 펜티엄3 에 처음 포함된 확장 기능<br />Float Point 및 비교 로직 등 다양한 연산<br />SSE 전용 128bit XMM 레지스터 8개 추가<br />MMX 와 달리 거의 모든 기능이 구현됨<br />
  4. 4. x86/x64 레지스터<br />
  5. 5. SIMD 연산<br />일반연산<br />1.0<br />2.0<br />3.0<br />4.0<br />1.0<br />5.0<br />6.0<br />7.0<br />8.0<br />5.0<br />6.0<br />8.0<br />10.0<br />12.0<br />6.0<br />
  6. 6. __m128자료형<br />typedef union __declspec(intrin_type) _CRT_ALIGN(16) __m128 {<br />float m128_f32[4];<br />unsigned __int64 m128_u64[2];<br />__int8 m128_i8[16];<br />__int16 m128_i16[8];<br />__int32 m128_i32[4];<br />__int64 m128_i64[2];<br />unsigned __int8 m128_u8[16];<br />unsigned __int16 m128_u16[8];<br />unsigned __int32 m128_u32[4];<br /> } __m128;<br /><ul><li>SIMD 연산을 하기 위한 자료형으로 XMM 레지스터와 1:1 대응이 되는 구조체
  7. 7. SSE 2 부터 새로이 추가된 __int64 와 double 을 지원하기 위한 __m128i, __m128d 자료형도 있음
  8. 8. 명령어에 따라 2,4,8,16 SIMD 연산이 수행될 수 있음. 구조체에는 어떤 데이터가 들어 있는지 알 수 없음</li></li></ul><li>시작하기 – SSE intrinsic<br />#include "stdafx.h“<br />#include <xmmintrin.h><br />void _tmain()<br />{<br />size_t count = 16 * 1024 * 1024; // 4 byte * 16M = 64MB<br /> // C version<br />float* a = newfloat[count];<br />float* b = newfloat[count];<br />for(size_ti=0; i<count;++i)<br /> {<br /> b[i] = a[i] + a[i];<br /> }<br /> // SSE version<br />__m128* a4 = (__m128*) _aligned_malloc(sizeof(float)*count, 16);<br />__m128* b4 = (__m128*) _aligned_malloc(sizeof(float)*count, 16);<br />for(size_ti=0; i<count/4;++i)<br /> {<br /> b4[i] = _mm_add_ps(a4[i], a4[i]);<br /> }<br />}<br />
  9. 9. 편하게 코딩하기<br />// 산술 연산자<br />__forceinline__m128operator+(__m128 l, __m128 r) { return_mm_add_ps(l,r); }<br />__forceinline__m128operator-(__m128 l, __m128 r) { return_mm_sub_ps(l,r); }<br />__forceinline__m128operator*(__m128 l, __m128 r) { return_mm_mul_ps(l,r); }<br />__forceinline__m128operator/(__m128 l, __m128 r) { return_mm_div_ps(l,r); }<br />__forceinline__m128operator+(__m128 l, float r) { return_mm_add_ps(l,_mm_set1_ps(r)); }<br />__forceinline__m128operator-(__m128 l, float r) { return_mm_sub_ps(l, _mm_set1_ps(r)); }<br />__forceinline__m128operator*(__m128 l, float r) { return_mm_mul_ps(l, _mm_set1_ps(r)); }<br />__forceinline__m128operator/(__m128 l, float r) { return_mm_div_ps(l, _mm_set1_ps(r)); }<br />// 논리 연산자<br />__forceinline__m128operator&(__m128 l, __m128 r) { return_mm_and_ps(l,r); }<br />__forceinline__m128operator|(__m128 l, __m128 r) { return_mm_or_ps(l,r); }<br />// 비교 연산자<br />__forceinline__m128operator<(__m128 l, __m128 r) { return_mm_cmplt_ps(l,r); }<br />__forceinline__m128operator>(__m128 l, __m128 r) { return_mm_cmpgt_ps(l,r); }<br />__forceinline__m128operator<=(__m128 l, __m128 r) { return_mm_cmple_ps(l,r); }<br />__forceinline__m128operator>=(__m128 l, __m128 r) { return_mm_cmpge_ps(l,r); }<br />__forceinline__m128operator!=(__m128 l, __m128 r) { return_mm_cmpneq_ps(l,r); }<br />__forceinline__m128operator==(__m128 l, __m128 r) { return_mm_cmpeq_ps(l,r); }<br />
  10. 10. SIMD 정말 4배 빠른가요?<br />// C 버젼 <br />for(size_ti=0; i<count;++i)<br />{<br /> b[i] = a[i] + a[i];<br />}<br />-> 실행 시간 49.267 ms<br />// Compiler Intrinsic 버젼<br />for(size_ti=0; i<count/4;++i)<br />{<br /> b4[i] = a4[i] + a4[i];<br />}<br />-> 실행 시간 47.927 ms<br />
  11. 11. 메모리 병목!!<br />a[0]<br />b[0]<br />+<br />a[1]<br />b[1]<br />+<br />a[2]<br />B[2]<br />+<br />a[3]<br />b[3]<br />+<br />a[4]<br />+<br />a[5]<br />+<br />a[0]<br />b[0]<br />a[1]<br />b[1]<br />a[2]<br />b[2]<br />a[3]<br />b[3]<br />a[4]<br />a[5]<br />a[6]<br />a[7]<br />+<br />+<br />+<br />+<br />+<br />+<br />+<br />
  12. 12. 연산량을 늘리자! sinf()<br />// sin(a) = a – (a^3)/3! + (a^5)/5! – (a^7)/7! …<br />float req_3f = 1.0f / (3.0*2.0*1.0);<br />float req_5f = 1.0f / (5.0*4.0*3.0*2.0*1.0);<br />float req_7f = 1.0f / (7.0*6.0*5.0*4.0*3.0*2.0*1.0);<br />for(size_ti=0; i<count; ++i)<br />{<br /> b[i] = a[i] <br /> - a[i]*a[i]*a[i]*req_3f <br /> + a[i]*a[i]*a[i]*a[i]*a[i]*req_5f <br /> - a[i]*a[i]*a[i]*a[i]*a[i]*a[i]*a[i]*req_7f;<br />}<br />-> 실행 시간 111. ms<br />
  13. 13. C 언어의 연산 병목<br />a[0]<br />b[0]<br />+<br />a[1]<br />b[1]<br />+<br />a[2]<br />b[2]<br />+<br />a[3]<br />b[3]<br />+<br />a[4]<br />+<br />a[0]<br />a[1]<br />a[2]<br />a[3]<br />a[4]<br />b[0]<br />b[1]<br />b[2]<br />b[3]<br />+<br />+<br />+<br />+<br />+<br />
  14. 14. SSE 버젼의 sinf() <br />// sin(a) = a – (a^3)/3! + (a^5)/5! – (a^7)/7! …<br />__m128 req_3f4 = _mm_set1_ps(req_3f);<br />__m128 req_5f4 = _mm_set1_ps(req_5f);<br />__m128 req_7f4 = _mm_set1_ps(req_7f);<br />for(size_ti=0; i<count/4; ++i)<br />{<br /> b4[i] = a4[i] <br /> - a4[i]*a4[i]*a4[i]*req_3f4 <br /> + a4[i]*a4[i]*a4[i]*a4[i]*a4[i]*req_5f4 <br /> - a4[i]*a4[i]*a4[i]*a4[i]*a4[i]*a4[i]*a4[i]*req_7f4;<br />}<br />-> 실행 시간 48.939 ms<br />
  15. 15. SSE는 아직도 메모리 병목!! <br />a[0,1,2,3]<br />+<br />b[0,1,2,3]<br />a[4,5,6,7]<br />+<br />b[4,5,6,7]<br />a[8,9,10,11]<br />+<br />b[8,9,10,11]<br />a[12,13,14,15]<br />+<br />b[12,13,14,15]<br />a[16,17,18,19]<br />a[0,1,2,3]<br />b[0,1,2,3]<br />a[4,5,6,7]<br />b[4,5,6,7]<br />a[8,9,10,11]<br />b[8,9,10,11]<br />a[12,13,14,15]<br />b[12,13,14,15]<br />a[16,17,18,19]<br />+<br />+<br />+<br />+<br />
  16. 16. a+a과 sin() 연산 시간이 같다 ?<br /><ul><li>C 에서 a[i] + b[i] 를 구성하는데 2.5 명령어로 실행되었고 sin() 은 19.5 명령어로 실행 (Loop Unrolling)
  17. 17. SSE 에서 a4[i] + b4[i] 를 구성하는데 6 명령어로 실행되었고 sin() 은 29 명령어로 실행</li></li></ul><li>컴파일러가 최적화 안해줍니까?<br />컴파일러의 SSE 최적화 옵션으로 빨라질 수 있다. <br />FPU 는 구조적인 문제로 SSE 유닛보다 느리다.<br /><ul><li>x64 컴파일러는 FPU 를 사용하지 않고 SSE 를 기본으로 사용한다.
  18. 18. 그러나 컴파일러는 Vectorization을 잘 못한다.</li></li></ul><li>더 복잡한 계산을 걸어봅시다!<br />
  19. 19. 몇배나 빠르다고요?<br />
  20. 20. _mm_stream_ps()<br />// C 버젼 <br />for(size_ti=0; i<count;++i)<br />{<br /> b[i] = a[i] + a[i];<br />}<br />-> 실행 시간 49.267 ms<br />// a+a stream 버젼<br />for(size_ti=0; i<count/4;++i)<br />{<br />_mm_stream_ps((float*)(b4+i), _mm_add_ps(a4[i], a4[i]));<br />}<br />-> 실행 시간 30.114 ms<br />
  21. 21. CPU<br />_mm_stream_ps() 의 작동<br />Excution Unit<br />L1 Cache<br />L2 Cache<br />WC Buffer<br />Memory BUS<br />Memory<br />
  22. 22. _mm_stream_ps() 는 빠르다 !!<br />Move Aligned Four Packed Single-FP Non Temporal<br /><ul><li>CPU 캐쉬를 거치지 않고 WC 메모리에 데이터를 전송한다.
  23. 23. 쓰기 순서를 보장하지 않으므로 쓰고 바로 읽으면 안됨</li></li></ul><li>그렇다면 sin() 도 빨라질까 ?<br />// SSE intrinsic<br />for(size_ti=0; i<count/4; ++i)<br />{<br /> b4[i] = a4[i] - a4[i]*a4[i]*a4[i]*req_3f4 <br /> + a4[i]*a4[i]*a4[i]*a4[i]*a4[i]*req_5f4 <br /> - a4[i]*a4[i]*a4[i]*a4[i]*a4[i]*a4[i]*a4[i]*req_7f4;<br />}<br />-> 실행 시간 48.939 ms<br />// SSE intrinsic + _mm_stream_ps()<br />for(size_ti=0; i<count/4; ++i)<br />{<br />_mm_stream_ps( (float*)(b4+i), <br /> a4[i] <br /> - a4[i]*a4[i]*a4[i]*req_3f4 <br /> + a4[i]*a4[i]*a4[i]*a4[i]*a4[i]*req_5f4 <br /> - a4[i]*a4[i]*a4[i]*a4[i]*a4[i]*a4[i]*a4[i]*req_7f4 );<br />}<br />-> 실행 시간 32.081 ms<br />
  24. 24. Stream 을 추가한 그래프 !!<br />
  25. 25. 같은 시간에 더 많은 일을 합시다!!<br /> float Read + Write 시간 : 2.896 ns<br />__m128 Read + Write 시간 : 11.214 ns<br />__m128 Read + Stream 시간 : 6.977 ns<br />
  26. 26. SSE 프로그래밍<br />메모리 접근 시간이 길어지고 연산시간이 짧아짐에 따라 더 많은 계산을 할 수 있다.<br />요즘 CPU는 Out-of-Order 로 인해 대부분 비동기 실행을 한다. 적극 이용하자.<br />병렬화와 병목 문제는 GPGPU 연산에도 동일하게 적용된다. 미래를 대비하자.!!<br />
  27. 27. SSE 를 적용한 예제들<br />
  28. 28. SSE 를 사용한 CPU Skinning<br />Vertex : 1024 * 1024 <br />Bone : 200<br />4 weight per vertex + normal + tangent<br />SSE 컴파일 옵션이 켜진 C, SSE최적화<br />스키닝 없는 C 루프 복사, SSE 루프 복사, memcpy()<br />
  29. 29. C Skinning Code<br />// Optimized C Version <br />D3DXMATRIX m = b[in->index[0]] * in->blend[0] <br /> + b[in->index[1]] * in->blend[1] <br /> + b[in->index[2]] * in->blend[2] <br /> + b[in->index[3]] * in->blend[3];<br />out->position.x = in->position.x*m._11 + in->position.y*m._21 + in->position.z*m._31 + m._41;<br />out->position.y = in->position.x*m._12 + in->position.y*m._22 + in->position.z*m._32 + m._42;<br />out->position.z = in->position.x*m._13 + in->position.y*m._23 + in->position.z*m._33 + m._43;<br />out->normal.x = in->normal.x*m._11 + in->normal.y*m._21 + in->normal.z*m._31;<br />out->normal.y = in->normal.x*m._12 + in->normal.y*m._22 + in->normal.z*m._32;<br />out->normal.z = in->normal.x*m._13 + in->normal.y*m._23 + in->normal.z*m._33;<br />out->tangent.x = in->tangent.x*m._11 + in->tangent.y*m._21 + in->tangent.z*m._31;<br />out->tangent.y = in->tangent.x*m._12 + in->tangent.y*m._22 + in->tangent.z*m._32;<br />out->tangent.z = in->tangent.x*m._13 + in->tangent.y*m._23 + in->tangent.z*m._33;<br />
  30. 30. SSE Skinning Code<br />// SSE Code<br />__m128 b0 = _mm_set_ps1(in->blend[0]);<br />__m128 b1 = _mm_set_ps1(in->blend[1]);<br />__m128 b2 = _mm_set_ps1(in->blend[2]);<br />__m128 b3 = _mm_set_ps1(in->blend[3]);<br />__m128* m[4] = { (__m128*)( matrix+in->index[0] ), <br /> (__m128*)( matrix+in->index[1] ), <br /> (__m128*)( matrix+in->index[2] ), <br /> (__m128*)( matrix+in->index[3] ) };<br />__m128 m0 = m[0][0]*b0 + m[1][0]*b1 + m[2][0]*b2 + m[3][0]*b3;<br />__m128 m1 = m[0][1]*b0 + m[1][1]*b1 + m[2][1]*b2 + m[3][1]*b3;<br />__m128 m2 = m[0][2]*b0 + m[1][2]*b1 + m[2][2]*b2 + m[3][2]*b3;<br />__m128 m3 = m[0][3]*b0 + m[1][3]*b1 + m[2][3]*b2 + m[3][3]*b3;<br />_mm_stream_ps( out->position, m0*in->position.x+m1*in->position.y+m2*in->position.z+m3 );<br />_mm_stream_ps( out->normal, m0*in->normal.x+m1*in->normal.y+m2*in->normal.z );<br />_mm_stream_ps( out->tangent, m0*in->tangent.x+m1*in->tangent.y+m2*in->tangent.z );<br />
  31. 31. SSE Skinning 결과<br />memcpy() 시간의 80% 로 스키닝을 할 수 있다.<br />파티클, UI 등에 유용하게 사용할 수있다.<br />Dynamic VB 를 쓰는 동안 계산을 추가로 할 수 있다.<br />
  32. 32. SSE를 사용한 KdTree<br /><ul><li>Ray-Trace 에 특화된 Binary Tree (Axis Aligned BSP)
  33. 33. Deep-Narrow Tree 를 만들어야 효율이 좋아지므로 노드가 무척 많아진다.
  34. 34. Tree Node 방문이 전체 처리 시간의 90% 을 차지한다.</li></li></ul><li>kDTree Traverse<br />
  35. 35. kDTree Packet Traverse<br />
  36. 36. KdTree테스트 결과<br />
  37. 37. Scaleform과 SSE<br />Flash 파일을 3D 가속을 받으며 실행 가능하도록 만들어진 라이브러리<br />Direct3D/OpenGL 및 다양한 렌더링 라이브러리 지원<br />현재 프로젝트의 UI 제작에 사용<br />209개 파일 65147 Line 의 Acton Script 와 DXT5 79MB UI 이미지<br />
  38. 38. Scaleform 3.1 의 문제점<br />복잡한 swf들을 다수 사용할 경우 CPU 사용률이 상당히 높다.<br />높은 자유도가 GPU에 최적화 되기 어려운 UI 를 만들게 한다.<br />GRendererD3D9 은예제 코드에 가깝고 개발시 H/W 특성이 고려되지 않았다.<br />
  39. 39. Scaleform개선 방향<br />Client<br />GFx<br />Client<br />GFx<br />GFxQueue<br />Direct3D<br />Direct3D<br />GFxMoveView::Advance()<br />GFxMoveView::Advance()<br />SceneMgr::DrawScene()<br />GFxMoveView::DisplayMT()<br />SceneMgr::DrawScene()<br />GFxQueue::DrawPrim()<br />GFxMoveView::Display()<br />GFxQueue::Flush()<br />ID3DDevice::DrawPrim()<br />5~15ms/frame<br />ID3DDevice::DrawPrim()<br />
  40. 40. GFxQueue의 Batch 합치기 기능<br />Batch 합치기를 하기 위해 Vertex 를 Queue 에 넣을때 Transform (TnL) 을 미리 처리<br />Render State, Texture State 를 체크해서 중복된 렌더링 재설정을 방지<br />Scene 에서 벗어난 Shape 들안그리는 기능 추가<br />CPU로 대체된 VertexShader는 삭제, Pixel Shader도 Batch 합치기를 위해 수정<br />
  41. 41. Transform 코드<br />caseVS_XY16iCF32:<br />{<br /> XY16iCF32_VERTEX* input = (XY16iCF32_VERTEX*)src + start;<br />for(UINT i=0; i<count; ++i){<br /> //output->pos.x = g_x + (input->x * vertexShaderConstant[0].x + input->y * vertexShaderConstant[1].x + vertexShaderConstant[2].x) * g_width;<br /> //output->pos.y = g_y - (input->x * vertexShaderConstant[0].y + input->y * vertexShaderConstant[1].y + vertexShaderConstant[2].y) * g_height;<br /> //output->pos.z = 1;<br /> //output->pos.w = 1;<br /> //output->color = FlipColor(input->color);<br /> //output->factor = FlipColor(input->factor);<br /> //output->tc0.x = input->x * vertexShaderConstant[3].x + input->y * vertexShaderConstant[4].x + vertexShaderConstant[5].x;<br /> //output->tc0.y = input->x * vertexShaderConstant[3].y + input->y * vertexShaderConstant[4].y + vertexShaderConstant[5].y;<br /> //aabb.AddPoint(output->pos);<br />__m128 pos = g_pos + ( input->x*vertexShaderConstant[0] + input->y*vertexShaderConstant[1] + vertexShaderConstant[2] ) * g_size;<br />_mm_storeu_ps(output->pos, pos);<br />__m128i colors = _mm_loadl_epi64((__m128i*)&input->color);<br />__m128iunpack = _mm_unpacklo_epi8(colors, g_zero);<br />__m128ishuffle = _mm_shufflelo_epi16(unpack, _MM_SHUFFLE(3,0,1,2));<br /> shuffle = _mm_shufflehi_epi16(shuffle, _MM_SHUFFLE(3,0,1,2));<br />__m128ipacked = _mm_packus_epi16(shuffle, g_zero);<br />_mm_storel_epi64((__m128i*)&output->color, packed);<br /> __m128tc = input->x*vertexShaderConstant[3] + input->y*vertexShaderConstant[4] + vertexShaderConstant[5];<br />_mm_storeu_ps(output->tc0, tc);<br />aabb_min = _mm_min_ps(aabb_min, pos);<br />aabb_max = _mm_max_ps(aabb_max, pos);<br /> ++output;<br /> ++input;<br />}<br />}<br />
  42. 42. GFxQueue Draw Call 횟수<br />
  43. 43. GFx Renderer 코멘트<br />GRenderD3D9 코드가 구리다. 프로그래머라면 찬찬히 분석한다음 여러군데 손을 봐두자.<br />UI 아티스트는 GPU 최적화에신경쓰지 않는다. 초기 단게부터 적절한 레이아웃과 컴포넌트를 설계해두자.<br />GFxExport에서 DXTn포맷을 무조건 2의 배수로 Resize 해버려 저장하는 경우가 있다.<br />GFxExport에서 Texture Atlas 기능을 쓰는 것도 최적화에 큰 도움이 된다. <br />
  44. 44. ?<br />

×