O slideshow foi denunciado.
Utilizamos seu perfil e dados de atividades no LinkedIn para personalizar e exibir anúncios mais relevantes. Altere suas preferências de anúncios quando desejar.

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

  • Entre para ver os comentários

이권일 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 />

×