SSE 를 이용한 최적화와 실제 사용 예
이권일EA Seoul Studio (BFO)
발표 대상• C/C++ 프로그래머
• H/W 및 최적화에 관심 있는 자
• GPGPU 를 준비하는 자
SSE (SIMD Streaming Extension)
• 1999 년 펜티엄 3 에 처음 포함된 확장 기능
• Float Point 및 비교 로직 등 다양한 연산
• SSE 전용 128bit XMM 레지스터 8 개 추가
• MMX 와 달리 거의 모든 기능이 구현됨
x86/x64 레지스터
SIMD 연산
1.0 2.0 3.0 4.0
5.0 6.0 7.0 8.0
6.0 8.0 10.0 12.0
일반 연산
1.0
5.0
6.0
__m128 자료형
• SIMD 연산을 하기 위한 자료형으로 XMM 레지스터와 1:1 대응이 되는 구조체
• SSE 2 부터 새로이 추가된 __int64 와 double 을 지원하기 위한 __m128i, __m128d 자료형도 있음
• 명령어에 따라 2,4,8,16 SIMD 연산이 수행될 수 있음 . 구조체에는 어떤 데이터가 들어 있는지 알 수 없음
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;
시작하기 – SSE intrinsic#include "stdafx.h“#include <xmmintrin.h>
void _tmain(){
size_t count = 16 * 1024 * 1024; // 4 byte * 16M = 64MB
// C versionfloat* a = new float[count];float* b = new float[count];for(size_t i=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_t i=0; i<count/4;++i){b4[i] = _mm_add_ps(a4[i], a4[i]);}
}
편하게 코딩하기// 산술 연산자__forceinline __m128 operator+(__m128 l, __m128 r) { return _mm_add_ps(l,r); }__forceinline __m128 operator-(__m128 l, __m128 r) { return _mm_sub_ps(l,r); }__forceinline __m128 operator*(__m128 l, __m128 r) { return _mm_mul_ps(l,r); }__forceinline __m128 operator/(__m128 l, __m128 r) { return _mm_div_ps(l,r); }
__forceinline __m128 operator+(__m128 l, float r) { return _mm_add_ps(l,_mm_set1_ps(r)); }__forceinline __m128 operator-(__m128 l, float r) { return _mm_sub_ps(l, _mm_set1_ps(r)); }__forceinline __m128 operator*(__m128 l, float r) { return _mm_mul_ps(l, _mm_set1_ps(r)); }__forceinline __m128 operator/(__m128 l, float r) { return _mm_div_ps(l, _mm_set1_ps(r)); }
// 논리 연산자__forceinline __m128 operator&(__m128 l, __m128 r) { return _mm_and_ps(l,r); }__forceinline __m128 operator|(__m128 l, __m128 r) { return _mm_or_ps(l,r); }
// 비교 연산자__forceinline __m128 operator<(__m128 l, __m128 r) { return _mm_cmplt_ps(l,r); }__forceinline __m128 operator>(__m128 l, __m128 r) { return _mm_cmpgt_ps(l,r); }__forceinline __m128 operator<=(__m128 l, __m128 r) { return _mm_cmple_ps(l,r); }__forceinline __m128 operator>=(__m128 l, __m128 r) { return _mm_cmpge_ps(l,r); }__forceinline __m128 operator!=(__m128 l, __m128 r) { return _mm_cmpneq_ps(l,r); }__forceinline __m128 operator==(__m128 l, __m128 r) { return _mm_cmpeq_ps(l,r); }
SIMD 정말 4 배 빠른가요 ?
// C 버젼 for(size_t i=0; i<count;++i){
b[i] = a[i] + a[i];}-> 실행 시간 49.267 ms
// Compiler Intrinsic 버젼for(size_t i=0; i<count/4;++i){
b4[i] = a4[i] + a4[i];}-> 실행 시간 47.927 ms
메모리 병목 !!a[0] b[0]+
+
a[1] b[1]+
a[2] B[2]+
a[3] b[3]+
a[4] +
a[5] +
a[0] b[0]
+
a[1] b[1]
+
a[2] b[2]
+
a[3] b[3]
+
a[4] a[5]
+
a[6] a[7]
+
연산량을 늘리자 ! 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_t i=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
C 언어의 연산 병목a[0] b[0]+
a[1] b[1]
a[2] b[2]
a[3] b[3]
+
+
+
a[4] +
a[0]a[1]a[2]a[3]a[4]b[0] b[1] b[2] b[3]
+ + + + +
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_t i=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 는 아직도 메모리 병목 !! a[0,1,2,3] + b[0,1,2,3]
a[4,5,6,7] + b[4,5,6,7]
a[8,9,10,11] + b[8,9,10,11]
a[12,13,14,15] + b[12,13,14,15]
a[16,17,18,19]
a[0,1,2,3] b[0,1,2,3]a[4,5,6,7] b[4,5,6,7]a[8,9,10,11] b[8,9,10,11]a[12,13,14,15] b[12,13,14,15]a[16,17,18,19]
+ + + +
a+a 과 sin() 연산 시간이 같다 ?
• C 에서 a[i] + b[i] 를 구성하는데 2.5 명령어로 실행되었고 sin() 은 19.5 명령어로 실행 (Loop Unrolling)
• SSE 에서 a4[i] + b4[i] 를 구성하는데 6 명령어로 실행되었고 sin() 은 29 명령어로 실행
a[i]+b[i] sin()0
20
40
60
80
100
120
49.267
111.273
47.927 48.939CSSE intirinsic
컴파일러가 최적화 안해줍니까 ?
• 컴파일러의 SSE 최적화 옵션으로 빨라질 수 있다 .
• FPU 는 구조적인 문제로 SSE 유닛보다 느리다 .
• x64 컴파일러는 FPU 를 사용하지 않고 SSE 를 기본으로 사용한다 .
• 그러나 컴파일러는 Vectorization 을 잘 못한다 .
a[i]+b[i] sin()0
20
40
60
80
100
120
49.267
111.273
49.267
71.331
47.927 48.939
CC SSE Opt.SSE intirinsic
더 복잡한 계산을 걸어봅시다 !
1 3 5 7 9 11 13 15 17
C FPU 48.585 49.846 74.549 111.273 147.32 193.387 240.348 273.087 342.971
C SSE Optimize 48.998 49.253 50.862 71.331 93.255 148.949 200.072 256.95 334.526
SSE intrinsic 47.033 47.863 48.392 48.939 49.773 58.705 80.938 118.047 183.622
1 3 5 7 9 11 13 15 170
50
100
150
200
250
300
350
400
C FPU C SSE Optimize SSE intrinsic
몇배나 빠르다고요 ?
1 3 5 7 9 11 13 15 170
0.5
1
1.5
2
2.5
3
3.5
X 1.0 X 1.0 X 1.1
X 1.5
X 1.9
X 2.5 X 2.5
X 2.2
X 1.8
X 1.0 X 1.0
X 1.5
X 2.3
X 3.0
X 3.3
X 3.0
X 2.3
X 1.9
X 1.0 X 1.0
X 1.5 X 1.6 X 1.6
X 1.3 X 1.2
X 1.1 X 1.0
FPU / SSE Optimize FPU / intrinsic C SSE / intrinsic
_mm_stream_ps()
// C 버젼 for(size_t i=0; i<count;++i){
b[i] = a[i] + a[i];}-> 실행 시간 49.267 ms
// a+a stream 버젼for(size_t i=0; i<count/4;++i){
_mm_stream_ps((float*)(b4+i), _mm_add_ps(a4[i], a4[i]));}-> 실행 시간 30.114 ms
CPU
_mm_stream_ps() 의 작동Excution
Unit
L1 Cache
L2 Cache
Memory BUS
Memory
WC Buffer
_mm_stream_ps() 는 빠르다 !!
• Move Aligned Four Packed Single-FP Non Temporal
• CPU 캐쉬를 거치지 않고 WC 메모리에 데이터를 전송한다 .
• 쓰기 순서를 보장하지 않으므로 쓰고 바로 읽으면 안됨
memcpy() b4[i]=a4[i] _mm_stream_ps()0
5
10
15
20
25
30
35
40
45
50 47.17 ms 47.03 ms
29.26 ms
memcpy()
b4[i]=a4[i]
_mm_stream_ps()
0 500 1000 1500 2000 2500 3000 3500 4000 4500 5000
2,713.6 MB/s
2,721.5 MB/s
4,374.1 MB/s
그렇다면 sin() 도 빨라질까 ?// SSE intrinsicfor(size_t i=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_t i=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
Stream 을 추가한 그래프 !!
1 3 5 7 9 11 13 15 17C FPU 48.585 49.846 74.549 111.273 147.32 193.387 240.348 273.087 342.971
C SSE Optimize 48.998 49.253 50.862 71.331 93.255 148.949 200.072 256.95 334.526SSE intrinsic 47.033 47.863 48.392 48.939 49.773 58.705 80.938 118.047 183.622
SSSE intrinsic+stream
29.263 29.83 30.636 32.081 39.067 56.332 80.373 117.28 183.932
1 3 5 7 9 11 13 15 170
50
100
150
200
250
300
350
400
C FPU C SSE Optimize SSE intrinsic SSSE intrinsic+stream
같은 시간에 더 많은 일을 합시다 !!
• float Read + Write 시간 : 2.896 ns• __m128 Read + Write 시간 : 11.214 ns• __m128 Read + Stream 시간 : 6.977 ns
1 3 5 7 9 11 13 15 17
C FPU48.585 49.846 74.549
111.273
147.32193.38
7240.34
8273.08
7342.97
12.5 8.5 13.5 19.5 25.75 32 39 43 49
C SSE Optimize48.998 49.253 50.862 71.331 93.255
148.949
200.072
256.95334.52
62.5 7.5 13.625 18.5 25.875 31 36 41 47
intrinsic47.033 47.863 48.392 48.939 49.773 58.705 80.938
118.047
183.622
5 11 19 29 41 55 71 91 114
intrinsic+stream29.263 29.83 30.636 32.081 39.067 56.332 80.373 117.28
183.932
5 11 19 29 41 55 71 91 113
Hand Opt.30.251 30.403 31.047 32.554 32.71 36.161 45.853 64.715 97.201
8 12 16 20 24 28 32 36 40
SSE 프로그래밍• 메모리 접근 시간이 길어지고 연산 시간이
짧아짐에 따라 더 많은 계산을 할 수 있다 .
• 요즘 CPU 는 Out-of-Order 로 인해 대부분 비동기 실행을 한다 . 적극 이용하자 .
• 병렬화와 병목 문제는 GPGPU 연산에도 동일하게 적용된다 . 미래를 대비하자 .!!
SSE 를 적용한 예제들
SSE 를 사용한 CPU Skinning
• Vertex : 1024 * 1024
• Bone : 200
• 4 weight per vertex + normal + tangent
• SSE 컴파일 옵션이 켜진 C, SSE 최적화
• 스키닝 없는 C 루프 복사 , SSE 루프 복사 , mem-cpy()
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 를 쓰는 동안 계산을 추가로 할 수 있다 .
C++ Skinning SSE Skinning C++ Copy SSE Copy memcpy 0
10
20
30
40
50
60
70
8070.648972
27.648912
34.219357
26.265869
34.247543
SSE 를 사용한 KdTree
• Ray-Trace 에 특화된 Binary Tree (Axis Aligned BSP)
• Deep-Narrow Tree 를 만들어야 효율이 좋아지므로 노드가 무척 많아진다 .
• Tree Node 방문이 전체 처리 시간의 90% 을 차지한다 .
kDTree Traverse
kDTree Packet Traverse
KdTree 테스트 결과
left node visit right node visit total node visit total leaf visit triangle intersect
Compiler Opt. SSE Intrinsic Ratio
실행시간 0.1564772 0.075881 2.0621
MRays/Sec 4.09 8.43 0.4852
call 640000 160000 4.0000
left node visit 6932440 1940425 3.5726
right node visit 6017289 1686695 3.5675
total node visit 13589729 3787120 3.5884
total leaf visit 17360227 5092768 3.4088
triangle intersect 608505 238760 2.5486
Scaleform 과 SSE
• Flash 파일을 3D 가속을 받으며 실행 가능하도록 만들어진 라이브러리
• Direct3D/OpenGL 및 다양한 렌더링 라이브러리 지원
• 현재 프로젝트의 UI 제작에 사용
• 209 개 파일 65147 Line 의 Acton Script 와 DXT5 79MB UI 이미지
Scaleform 3.1 의 문제점 • 복잡한 swf 들을 다수 사용할 경우 CPU
사용률이 상당히 높다 .
• 높은 자유도가 GPU 에 최적화 되기 어려운 UI 를 만들게 한다 .
• GRendererD3D9 은 예제 코드에 가깝고 개발시 H/W 특성이 고려되지 않았다 .
5~15ms/frame
Scaleform 개선 방향Client GFx
GFxMoveView::Advance()
GFxMoveView::Display()
ID3DDevice::DrawPrim()
SceneMgr::DrawScene()
Client GFx GFxQueue
GFxMoveView::Advance()
Direct3D
GFxMoveView::DisplayMT()
GFxQueue::DrawPrim()
SceneMgr::DrawScene()
GFxQueue::Flush() ID3DDevice::DrawPrim()
Direct3D
GFxQueue 의 Batch 합치기 기능
• Batch 합치기를 하기 위해 Vertex 를 Queue 에 넣을때 Transform (TnL) 을 미리 처리
• Render State, Texture State 를 체크해서 중복된 렌더링 재설정을 방지
• Scene 에서 벗어난 Shape 들 안그리는 기능 추가
• CPU 로 대체된 VertexShader 는 삭제 , Pixel Shader 도 Batch 합치기를 위해 수정
Transform 코드case VS_XY16iCF32:{
XY16iCF32_VERTEX* input = (XY16iCF32_VERTEX*)src + start;for(UINT i=0; i<count; ++i) {
//output->pos.x = g_x + (input->x * vertexShaderConstant[0].x + input->y * vertexShaderConstant[1].x + ver-texShaderConstant[2].x) * g_width;
//output->pos.y = g_y - (input->x * vertexShaderConstant[0].y + input->y * vertexShaderConstant[1].y + ver-texShaderConstant[2].y) * g_height;
//output->pos.z = 1;//output->pos.w = 1;//output->color = FlipColor(input->color);//output->factor = FlipColor(input->factor);//output->tc0.x = input->x * vertexShaderConstant[3].x + input->y * vertexShaderConstant[4].x + vertexShader-
Constant[5].x;//output->tc0.y = input->x * vertexShaderConstant[3].y + input->y * vertexShaderConstant[4].y + vertexShader-
Constant[5].y;//aabb.AddPoint(output->pos);
__m128 pos = g_pos + ( input->x*vertexShaderConstant[0] + input->y*vertexShaderConstant[1] + vertexShader-Constant[2] ) * g_size;
_mm_storeu_ps(output->pos, pos);__m128i colors = _mm_loadl_epi64((__m128i*)&input->color);__m128i unpack = _mm_unpacklo_epi8(colors, g_zero);__m128i shuffle = _mm_shufflelo_epi16(unpack, _MM_SHUFFLE(3,0,1,2));shuffle = _mm_shufflehi_epi16(shuffle, _MM_SHUFFLE(3,0,1,2));__m128i packed = _mm_packus_epi16(shuffle, g_zero);_mm_storel_epi64((__m128i*)&output->color, packed); __m128 tc = input->x*vertexShaderConstant[3] + input->y*vertexShaderConstant[4] + vertexShaderConstant[5];_mm_storeu_ps(output->tc0, tc);aabb_min = _mm_min_ps(aabb_min, pos);aabb_max = _mm_max_ps(aabb_max, pos);
++output;++input;
}}
GFxQueue Draw Call 횟수
0
100
200
300
400
500
600
700
800
GFxGFxQueued
GFx Renderer 코멘트• GRenderD3D9 코드가 구리다 . 프로그래머라면 찬찬히
분석한다음 여러군데 손을 봐두자 .
• UI 아티스트는 GPU 최적화에 신경쓰지 않는다 . 초기 단게부터 적절한 레이아웃과 컴포넌트를 설계해두자 .
• GFxExport 에서 DXTn 포맷을 무조건 2 의 배수로 Resize 해버려 저장하는 경우가 있다 .
• GFxExport 에서 Texture Atlas 기능을 쓰는 것도 최적화에 큰 도움이 된다 .
?
Top Related