SIMD Programming 01 - 시작 및 자료형
이전에 물리엔진을 만들어보겠다고 하고 성능향상에 대해서 고민하다가 발견한적이 있었습니다. 당시 자존감이 낮아서 그랬는지.. 약간의 코드를 보고 너무 어렵다고 생각하고 포기했던 기억이 있습니다. 이번에 공부할 수 있는 좋은 기회를 얻어 정리 겸 포스팅을 계획하고 있습니다.
기본적으로 참고해야할 레퍼런스 사이트는 이곳입니다.
https://software.intel.com/sites/landingpage/IntrinsicsGuide/
Intel® Intrinsics Guide
software.intel.com
1. SIMD란?
정말 많은 곳에서 SIMD의 개념을 설명합니다. 그래서 여기서는 간략하게 이야기하려고 합니다.
바로 "한가지 명령으로 많은 수의 데이터를 처리하자"(Single Instruction, Multiple Data)라는 개념입니다.
사실 이게 정의이고 설명의 끝이기도 합니다.
간단한 예시를 들어보겠습니다.
만약 a=10, b=20, c=30, d=40이라는 데이터가 있다고 가정해봅시다.
모든 데이터에 +10을 해주어야하는 상황이 생겼다면 일반적인 프로그래밍 방법은 이와 같을 것입니다.
a = a + 10;
b = b + 10;
c = c + 10;
d = d + 10;
모든 숫자에 직접 +10씩 해주는 방법이죠. 그렇다면 SIMD 프로그래밍에서는 어떨까요?
__m128 x = _mm_set_ps(a,b,c,d);
__m128 ten = _mm_set1_ps(10);
x = _mm_add_ps(x, ten);
맨 처음에 변수 선언부를 제외하면 _mm_add_ps()라는 함수 한번에 해결되는 것을 볼 수 있습니다.
이걸 보고 저는 처음에 이런 생각이 들었습니다.
"오? 한가지 명령으로 다수의 데이터를 처리한다고? 이거 여기도 써보고 저기도 써보고 좋겠다!"
알려주시는 교수님께서 따끔한 충고를 해주셨습니다.
"SIMD를 이용해서 프로그래밍할 수 있는 것은 좋은 무기가 되겠지만,
그 무기를 아무 때서나 사용하면 좋은 코드를 만들 수 없다.
먼저 자료구조와 알고리즘을 단단하게 쌓아올리고나서 SIMD를 적용할 수 있는지 확인해보아야 한다."
프로그램을 만들기 전에(코드를 짜기 전에) 먼저 구조와 알고리즘을 단단하게 보완해야한다는 것입니다.
굉장히 중요한 말이라서 서두에 이렇게 적습니다. 우리 모두 기억합시다!
본격적으로 공부를 시작하면 처음 보는 헤더와 자료형들을 볼 수 있습니다.
#include <xmmintrin.h>
int main()
{
__m128 a = _mm_set_ps(1, 2, 3, 4);
__m128 b = _mm_set_ps(5, 6, 7, 8);
__m128 c = _mm_add_ps(a, b);
return 0;
}
저도 처음 봤을 때 거부감부터 들었지만, 천천히 보면 그리 어려운 코드는 아닙니다.
#include <xmmintrin.h>
아주 기본적인 헤더입니다. 이후에는 다른 헤더파일들도 사용합니다.
(이후 발전된 헤더를 사용합니다만 여기서는 기본적인 xmmintrin.h을 썼습니다.)
__m128 a = _mm_set_ps(1, 2, 3, 4);
여기서 새로운 친구를 2명이나 만납니다. __m128과 _mm_set_ps()입니다.
__m128은 하나의 자료형이라고 볼 수 있습니다.
와우! 그 말로만 듣던 union, 공용체를 발견할 수 있습니다.
C언어를 배우면서 보기 힘든 자료형입니다만 이런 곳에서 유용하게 사용되는 것을 볼 수 있습니다.
그리고 여기서 __m128에 float이 4개, unsigned int8이 16개 등등이 들어가는 것을 보고
우리는 __m128에서 "128"이 128 bits를 의미한다는 것을 알 수 있습니다!
(128 bits = 16 Bytes = 4(float) * 4 = 8(unsigned int8 = unsigned char) * 16)
이에 대해서는 나중에 더 알아보도록 합시다.
다음으로는 _mm_set_ps()가 있습니다. 이 녀석은 어떤 함수인지 알아보겠습니다.
우리는 __m128에 float 4개를 넣어서 리턴해주는 것을 알 수 있습니다.
"그럼 밑에 _mm_setr_ps()는 뭐야?"라고 물어보실 수 있습니다.
그냥 _mm_set_ps()를 이용하게 되면 4,3,2,1 순으로 저장되는 것을 볼 수 있습니다.
왜 이런 식으로 만들어졌을까요?
바로 인텔 CPU가 리틀 엔디언 방식으로 동작하기 때문입니다.
컴퓨터 구조 시간에 한번쯤 들어보았을 법한 리틀 엔디언, 빅 엔디언 이야기는 여기서 다루지 않겠습니다.
잘 정리된 링크를 추가했습니다.
https://ko.wikipedia.org/wiki/%EC%97%94%EB%94%94%EC%96%B8
엔디언 - 위키백과, 우리 모두의 백과사전
위키백과, 우리 모두의 백과사전. 엔디언(Endianness)은 컴퓨터의 메모리와 같은 1차원의 공간에 여러 개의 연속된 대상을 배열하는 방법을 뜻하며, 바이트를 배열하는 방법을 특히 바이트 순서(Byte order)라 한다. 엔디언은 보통 큰 단위가 앞에 나오는 빅 엔디언(Big-endian)과 작은 단위가 앞에 나오는 리틀 엔디언(Little-endian)으로 나눌 수 있으며, 두 경우에 속하지 않거나 둘을 모두 지원하는 것을 미들 엔디언(Middle
ko.wikipedia.org
다행히도 Intel의 큰 배려(?)로 _mm_setr_ps()는 우리가 생각하는 순서인 1,2,3,4 순으로 저장할 수도 있습니다.
이후 포스팅에서는 두 함수를 필요에 따라 사용하므로 주의 깊게 살펴보며 코드를 읽어 주시기 바랍니다.
* 추가하면 좋은 내용이나 부족한 부분은 댓글로 남겨주세요!