SIMD

Learning stuff 2013. 1. 29. 20:24

기글 하드웨어 특성상 소프트웨어 보단 하드웨어를 전문적으로 다루시는 분들이 많을 것입니다. 이 글은 x86의 SIMD 명령과 SIMD 명령의 개략적 원리 및 프로그래밍에 대해 설명하고 있습니다. 약간의 프로그래밍 지식이 필요한데, 아무 프로그래밍 언어 하나를 해보셨다면 이해하기 수월하실 겁니다. (암드 쪽은 철저히 배제했습니다. 왜냐면 제 맘이거든요. 3DNow!, SSE4a, SSE5쪽은 직접 문서를 참조하시기 바랍니다.)

1. SIMD란?

SIMD란 Single Instruction, Multiple Data의 약자로 Flynn의 컴퓨터 분류법에서 단일 명령으로 다중 데이터를 처리하는 컴퓨터를 일컫습니다.

SIMD는 대량의 데이터를 하나의 명령으로 한 번에 처리합니다. 쉽게 이야기 하면 세단과 덤프트럭의 비교라고 볼 수 있는데, 덤프트럭과 세단이 같은 속도로 달릴 수 있다고 치면 덤프트럭이 훨씬 더 많은 데이터를 담아갈 수 있는 이치와 같습니다.

이런 SIMD 구조의 명령(instruction)을 프로세서에 도입함으로써, 설계자들은 프로세서의 처리능력을 강화시키려 했습니다. 인텔의 경우 펜티엄 프로세서에 도입 된 MMX(MultiMedia eXtensions)을 시작으로 펜티엄 III에 도입된 SSE(Streaming SIMD Extensions), 샌디 브릿지에 도입된 AVX(Advanced Vector eXtensions)가 있고, 요즘 잘나가는 스마트 폰에 들어가는 ARM 코어에는 NEON이라는 명령이 있습니다.

2. 스칼라와 벡터

스칼라와 벡터는 수학/물리시간에 들어볼 법한 용어입니다. 컴퓨터에서도 사용되는 용어인데, 스칼라(scalar)는 임의의 타입(데이터 유형)을 갖는 단일 변수를 의미합니다. 즉, 일반적으로 우리가 변수라 부르는 것들은 모두 스칼라 데이터로 보시면 됩니다. 벡터(vector)는 임의지만 동일한 타입을 갖는 변수들의 모임을 의미합니다. 즉, 동질성 배열을 의미하는데, 일반적으로 여러 프로그래밍 언어에서 사용하는 배열과 의미가 같다고 보시면 됩니다.

3. SIMD 명령은 왜 필요한가?

SIMD 명령은 벡터 처리 성능을 끌어올리기 위해 만들어진 명령이라고 생각 하시면 쉽습니다. 어차피 스칼라 처리를 위하는데 굳이 SIMD가 필요할까요? 프로그래밍을 조금이라도 해보신 분은 알겠지만 배열의 데이터를 쉽게 처리하기 위해선 반복문을 필요로 합니다. 반복문은 스칼라 단위로 데이터를 처리합니다. 편의상 C언어(조금 더 정확하게는 C++)의 for문을 예로 들어보겠습니다.

float arr_a[4] = {1.0f,2.0f,3.0f,4.0f};
float arr_b[4] = {5.0f,6.0f,7.0f,8.0f};
float arr_c[4] = {0.0f};

for (int i = 0; i < 4; ++i)
arr_c[i] = arr_a[i] + arr_b[i];

다음과 같은 코드가 있습니다. arr_a, arr_b, arr_c라는 3개의 배열이 있고 이 배열은 각각 4개의 원소를 갖습니다. arr_a와 arr_b 배열에 있는 값을 각 인덱스끼리 대응하여 더한 다음 arr_c의 인덱스에 정확히 대입합니다. 즉, 다음 일러스트와 같은 과정을 거치게 됩니다.

simd1.png

그런데 위의 반복문은 스칼라 단위로 처리합니다. 즉, 덧셈을 4번을 반복하는 것입니다. 계산량이 적어서 별 것 아니라고 생각 하실 수도 있겠지만 스칼라 단위로 처리를 하게 되면 그만큼 다음과 같은 손해가 옵니다.

① 반복문은 반복 조건을 검사해서 탈출 판단을 해야 하므로 분기문(i < 4)을 사용합니다. 반복 횟수가 늘어나면 분기 횟수도 비례해서 늘어납니다. 그만큼 처리해야할 명령 수가 많아질뿐더러, 비록 분기 예측능력이 매우 좋아졌다지만 분기문은 프로세서 특성상 자주 사용되면 그만큼 성능에는 좋지 않은 영향을 끼치게 됩니다.

② 반복문의 조건 평가를 위해 증가식(++i)이 사용되는데, 반복 횟수가 늘어나면 증가식 처리 횟수 또한 비례해서 늘어납니다. 그만큼 처리해야할 명령 수가 늘어납니다.

전통적인 반복문은 이런 문제 때문에 성능을 끌어올리는데 걸림돌이 되곤 합니다. 그래서 프로세서 개발자들은 SIMD라는 아주 좋은 방법을 떠올렸습니다. SIMD는 그렇다면 어떤 점이 다를까요? 역시 프로그래밍 소스로 보겠습니다. 다음은 x86의 SSE 명령을 이용한 코드입니다.

__declspec(align(16)) float arr_a[4] = {1.0f,2.0f,3.0f,4.0f};
__declspec(align(16)) float arr_b[4] = {5.0f,6.0f,7.0f,8.0f};
__declspec(align(16)) float arr_c[4] = {0.0f};

__asm{
movaps xmm0, arr_a
movaps xmm1, arr_b
addps xmm0, xmm1
movaps arr_c, xmm0
}


어? for문이 아닌 이상한 코드가 들어갔습니다. 게다가 __declspec 또한 뭘까요.

먼저 declspec부터 설명하겠습니다. __declspec(align(16))은 지정한 배열을 할당 할 때, 16바이트 단위로 정렬(alignment)시켜서 할당 해달라는 의미입니다. 32비트 환경 기준으로 16바이트 단위로 할당할 경우, 이를 16진수로 주소를 나타내면 0x1234567X에서 X의 값이 무조건 0이라는 의미로 보시면 됩니다. 굳이 정렬 옵션을 안 넣고도 SIMD를 사용할 수 있으나 정렬을 하고 안하고의 성능이 최대 10%까지 차이나기도 하기 때문에 조금 귀찮아도 안 쓸 이유가 없죠. 결론은, 성능 때문에 저리 한 것입니다.

실제 SIMD 코드는 __asm이라는 중괄호 안에 들어있습니다. 프로그래밍 언어상에선 SIMD를 지원하지 않기 때문에 프로세서의 고유 명령의 경우엔 이런 식으로 직접 어셈블리어로 코딩해야 합니다. 물론 이게 100% 정확한 말은 아닙니다. 일반적으로 프로세서 제조사에선 프로그래밍 난이도를 낮추면서 컴파일러의 도움을 받게 하기위해 인트린직(intrinsic)이라는 함수들을 지원합니다. (다만 모든 인트린직을 완전히 다 쓰려면 인텔에서 만든 컴파일러가 필요합니다. 비주얼 스튜디오 2010의 경우엔 대부분 인트린직 사용이 가능했습니다.)

어셈블리어를 직접 사용하는 건 컴파일러의 특성을 타게 되므로 되도록 인트린직을 권하는 바이나 여기선 설명을 위해 직접 어셈블리어를 썼습니다. 다음 4줄이 그 어셈블리어입니다.

movaps xmm0, arr_a
movaps xmm1, arr_b
addps xmm0, xmm1
movaps arr_c, xmm0

xmm이라는 이름을 가진 것은 레지스터 이름입니다. 32비트 환경 기준으로 xmm0 ~ xmm7까지 총 8개가 존재합니다. movaps는 move aligned packed values를 뜻하는데, 즉 xmm0 레지스터가 한 번에 담을 수 있는 단위만큼 묶어서 데이터를 복사하겠단 뜻입니다. (오른쪽에서 왼쪽으로 전달됩니다.) 다루는 단위가 스칼라가 아니라 훨씬 더 큰 단위를 복사합니다.

simd2.png

둘째 줄은 똑같은 명령이니 설명을 생략하겠습니다. 실제 덧셈을 하는 명령은 addps(add packed values)입니다. 다른 아무것도 없이 명령 하나로 xmm0와 xmm1에 들어있는 내용을 수직으로 더해서 xmm0에 넣어줍니다. 반복문으로 4회에 걸쳐서 수행해야 하는 것을 명령 하나로 끝내고 있음을 알 수 있습니다.

simd3.png

이것이 SIMD 명령의 가장 큰 특징입니다. 명령 하나로 다수의 데이터들을 한방에 처리할 수 있죠. 마지막 줄의 movaps또한 생략하도록 하겠습니다. 반대로 생각하시면 되니까요.

그렇다면 SIMD 명령의 장점에 대해서 한번 생각 해 봅시다. 큰 배열의 경우 SIMD를 사용하면 반복문의 반복 횟수가 확 줄어듭니다. 분기문의 비교 횟수도 줄어들고 증가식(혹은 감소식)의 횟수도 줄어들어서 전체적인 성능이 더 나아지게 됩니다. 심심하니 직접 코딩해서 테스트 해보도록 하겠습니다.

simd4.png

좀 차이가 심한가요? 한 1000만번 계산 돌려보니 저런 결과가 나오는군요.. 물론 실제 프로그래밍 할 때는 현실적인 조건 때문에 저 정도의 차이는 잘 안 나옵니다.

4. x86 SIMD의 역사

SIMD가 어떤 원리로 돌아갔는지 대략적으로 배웠으니까 이번엔 x86이 그간 SIMD 명령을 어떻게 확장해 왔는지 살펴보겠습니다. 아마 ARM의 NEON도 큰 차이가 없을 거라고 보는데 자세한 사항은 제가 들은바가 없어서 ^^;

과거 펜티엄 프로세서에 처음 투입된 MMX가 x86 SIMD의 첫 시작입니다.

MMX는 한 번에 묶어서 다룰 수 있는 단위가 64비트였습니다. 64비트 크기로는 스칼라 변수를 8비트 8개, 16비트 4개, 32비트 2개를 다룰 수 있었죠. 하지만 MMX에는 크나큰 약점이 있었는데, FPU 레지스터와 공간을 공유했었습니다. FPU 레지스터는 그 크기가 80비트 였는데 MMX는 상위 16비트를 막고 64비트만 사용 한 것이죠. 어쨌든 이러한 점 때문에 MMX는 FPU와의 협업이 굉장히 힘들었고, 정수 벡터 처리만을 지원했기 때문에 부동소수점 수는 다룰 수 없었습니다.

이후 펜티엄 III에 SSE가 추가되면서 x86의 SIMD 명령에 큰 변화가 오게 됩니다. SSE는 실수 벡터 처리를 지원했고 그 크기 또한 MMX보다 두 배나 큰 128비트였습니다. 게다가 하드웨어적으로 완전히 분리된 레지스터이기 때문에 MMX처럼 FPU를 사용 못하는 문제는 걱정할 필요가 없어졌습니다.(그러나 펜티엄 III의 경우 FPU와 SSE 사이에 실행 자원을 공유했기 때문에 명령 자체는 동 사이클에 파이프라이닝을 통해 실행될 순 없었습니다.)

다만 SSE는 처음에 정수를 지원하지 않았고 이 문제는 펜티엄4에 추가된 SSE2에서 명령어를 확장시킴으로써 해결 했습니다. 따라서 8비트 16개, 16비트 8개, 32비트 4개, 64비트 2개를 처리할 수 있었습니다. SSE2는 정수 명령을 지원함으로써 MMX를 대체했고, 캐시 컨트롤과 메모리 정렬 명령들 몇 가지가 새로 추가됨으로써 연속되는 정보 처리에 좀 더 효율적인 코드를 짤 수 있도록 하였습니다.

이후 펜티엄4 프레스캇에 추가 된 SSE3는 기존의 수직적(vertical)인 데이터 처리구조를 탈피한 수평적(horizontal)인 데이터 처리구조를 지원하였습니다. SSE3 명령 중 addsubps라는 것이 있는데 이 명령의 일러스트를 한번 보죠.

simd5.png

오.. 덧셈만 하는게 아니라 뺄셈하고 덧셈을 번갈아 하고 있습니다. 아까 전에 우리가 본 SIMD 소스에서 addps를 addsubps로 바꾸고 결과물을 보면 다음과 같습니다.

simd6.png

이런 일이 있을 것 같은 조짐을 느끼셨지요?
이 외에 SSE3는 파형계산에 유용하거나 정수 벡터처리 및 하이퍼쓰레딩 지원 프로세서의 성능을 보완해주기 위한 몇몇 명령어가 추가되었습니다.

이후에 마이너한 SSE 명령으로 SSSE3(Supplemental SSE3)가 추가되었는데요. SSSE3는 SSE3의 확장판이라고 보시면 되요. 패키지 단위의 부호 변환이나 절댓값 변환, 셔플(패키지 안에서 스칼라 값의 위치를 뒤섞는 기법) 등이 생겼습니다.

이후에 펜린에 들어서서 SSE4.1, 네할렘에서 SSE4.2가 추가되었는데요. SSE4.1은 주로 수학적인 명령이 추가되었습니다. 두 피연산자의 차의 절댓값을 합하는 연산이라든지, 내적연산 같은 게 말이죠. SSE4.2는 문자열 비교연산과 CRC 명령이 추가 되었습니다.

이번엔 최근에 추가된 샌디브릿지의 AVX를 알아봅시다. AVX는 새로운 레지스터인 ymm이 추가되어 패키지 사이즈가 256비트로 늘었습니다. 그래서 32비트 데이터를 무려 8개나 담을 수 있습니다. 하지만 이놈도 인텔의 장난질에 놀아나는지 당장은 정수 벡터를 지원 하지 않습니다. (128비트까지만 됩니다. 아마 AVX2에서 지원 해줄 모양입니다. 이런..)

AVX의 특징이라면 3-피연산자/4-피연산자 연산의 추가인데, 이는 a = a + b가 아니라, c = a + b가 되어서 피연산자를 세 개/네 개로 지정 할 수 있습니다. 사실 저 피연산자 문제 때문에 SSE에선 프로그래밍 하는 게 약간 불편하긴 했는데 이렇게 바뀌었으니 좋지요. 그리고 여러 줄로 표기해야 하는 명령을 한 줄로 끝내게 되니 실행해야 하는 명령의 수도 줄어듭니다. 좀 더 자세한 글은 링크로 대신 하겠습니다.

http://gigglehd.com/zbxe/index.php?_filter=search&mid=infoboard&search_target=title&search_keyword=AVX&document_srl=5043210

5. 맺음말

지금까지 간단하게나마 SIMD에 대한 이해를 해보았는데, SIMD는 데이터 수준 병렬성(Data Level Parallelism)이 많이 나타나는 분야에서 자주 사용되곤 합니다. 주로 영상처리, 파형분석, 행렬, 내적연산 등에서 SIMD의 효과가 잘 드러납니다. 영상 처리라 함은 동영상 처리뿐만 아니라, 샤픈이나 엠보싱 같은 효과를 넣을 때도 행렬 연산을 거치기 때문에 SIMD가 가장 잘 먹히는 영역이죠.

이상으로 SIMD에 관한 이야기를 마치겠습니다.

 

 

출처 : gigglehd.com

'Learning stuff' 카테고리의 다른 글

verilog inout  (2) 2013.01.30
SRAM timing diagram  (0) 2013.01.30
verilog $setup, $hold, $width  (0) 2013.01.29
Cache Optimization  (0) 2013.01.25
matlab index  (0) 2012.12.12