이번에는 SIMD 프로그래밍으로 간단한 비교 연산을 해봅시다.
컴퓨터 공학을 전공 중이거나 프로그래밍을 해본 사람들이라면 비교 연산을 잘 알고 계실 겁니다.
그 중 AND가 뭘까요? C언어 등에서 비트연산자로(&)를 사용합니다.
두 피연산자의 비트가 같다면 1(혹은 true) 다르다면 0(혹은 false)를 리턴하죠.
그렇다면 인텔에서 제공하는 AND 연산은 어떻게 계산될까요
이전 글에서 소개했던 Intel Intrinsic을 찾아봅시다.
https://software.intel.com/sites/landingpage/IntrinsicsGuide/
Intel® Intrinsics Guide
software.intel.com
위 링크를 클릭합니다. 왼쪽 체크박스에서 SSE, SSE2를 체크하고,
검색창에 and를 검색하면 많은 함수중에 아래와 같은 함수를 찾을 수 있습니다.
"a와 b의 32비트마다 단정도(single precision) 실수 원소들마다 AND 연산을 한 결과를 저장하여 리턴한다"
__m128 자료형은 128비트의 크기를 가진다고 했었죠.
128/32 = 4, 그렇다면 위 함수는 단정도 실수인 float 원소 4개를 한번에 비교 연산할 수 있습니다.
그렇다면 또 다른 비교 연산 함수인 andnot은 뭘까요?
Operation에 적혀 있는 내용을 읽어보면 굉장히 독특한 연산임을 눈치채실 수 있습니다.
andnot 함수는 두 피연산자 중 앞의 피연산자에 NOT 연산을 하고나서 b와 AND 연산을 하는 것을 볼 수 있습니다.
andnot 함수가 어떤 결과를 리턴하는지 이해는 되셨을겁니다만..
아마 '도대체 이런 연산은 왜 만든거지? 어디다가 쓰는거야?'라는 생각을 하실 겁니다만...!
잠시 예시를 보기전에 같은지, 혹은 작거나 같은지 등의 부등호 연산을 하는 함수를 먼저 만나보겠습니다.
각 값마다 비교해서 같다면 0xffffffff, 다르면 0을 넣고 리턴합니다.
굳이 1이 아니라 0xffffffff를 리턴하는 이유는 무엇일까요?
AND나 OR 연산시에 모든 비트에 값을 대입하기 수월하기 때문입니다.
그렇다면 같을 때의 연산은 이렇다면 같거나 작을 때와 같은 부등호 상황은 어떻게 할까요?
cmplt (compare less than), cmpgt (compare greater than) 등의 함수가 있습니다.
드디어 예시코드를 한번 보겠습니다.
이런 C로 만들어진 코드가 있다고 생각해봅시다.
// p 배열에 from인 값을 to 값으로 변경하는 함수
void Change(float *p, float from, float to, int size) {
for (int i = 0; i < size; i++)
if(p[i] == from)
p[i] = to;
}
int main() {
float buf[4] = { 10, 20, 30, 40 };
Change(buf, 20, 50, 4); // 20을 50으로 변경!
}
위 코드를 SIMD 프로그래밍을 적용해서 만들어야한다라면 어떻게 해야할까요?
물론 저렇게 작은 배열에 하나의 원소만 바꾼다면 직접 바꾸는 것도 나쁘지는 않을 겁니다.
하지만 하나의 __m128 원소값을 접근하여 바꾸는 것은 전체 연산을 하는 것보다 비교적 비용이 비쌉니다..
그때문에 묶어놓은 변수를 풀어해치지 않고 해결하려는 생각을 해야합니다.
저는 이런 방식으로 풀었습니다.
다른 방법들도 있겠지만(cmpneq 등) 위의 함수들을 사용해야한다면 가장 쉬운 방법이라고 생각합니다.
void Change(float *p, float from, float to, int size)
{
__m128 vFrom = _mm_set_ps(from, from, from, from);
__m128 vTo = _mm_set_ps(to, to, to, to);
for (int i = 0; i < size; ++i)
{
__m128 vData = _mm_loadu_ps(p + i); // 배열 로드
// from과 같은 위치 찾기
__m128 vCmpData = _mm_cmpeq_ps(vData, vFrom);
// from과 같은 값이 아닌 곳의 값 유지
vData = _mm_andnot_ps(vCmpData, vData);
// to가 들어가야할 위치에 to를 넣기
__m128 vToData = _mm_and_ps(vCmpData, vTo);
// 위에서 만든 to만 있는 값과
// from과 같지 않은 유지된 값을 더해서
// 최종값 생성
vData = _mm_add_ps(vData, vToData);
// 배열에 저장
_mm_storeu_ps(p + i, vData);
}
}
for 루프 안쪽 코드를 한 줄씩 살펴보겠습니다.
__m128 vData = _mm_loadu_ps(p + i); // 배열 로드
배열 혹은 포인터에서 값을 읽어오는 함수입니다. 그냥 load 함수도 있지만 여기서는 loadu 함수를 사용했습니다.
// from과 같은 위치 찾기
__m128 vCmpData = _mm_cmpeq_ps(vData, vFrom);
from 값(20)이 어느 위치에 있는지 알기 위한 코드입니다.
vData와 vFrom이 같은지 비교하면 vCmpData = (0, 0xffffffff, 0, 0)이 들어 있겠네요.(역순 고려x)
// from과 같은 값이 아닌 곳의 값 유지
vData = _mm_andnot_ps(vCmpData, vData);
위에서 어느 위치에 from값(20)이 들어있는지 찾았으니 from이 아닌 부분의 값을 유지해야겠죠.
이럴 때 andnot 함수를 사용할 수 있을 겁니다.
결과 : NOT(vCmpData) AND vData = (10, 0, 30, 40)
// to가 들어가야할 위치에 to를 넣기
__m128 vToData = _mm_and_ps(vCmpData, vTo);
to(50)이 들어가야할 위치에만 어떻게 to값을 넣을 수 있을까요?
어느 위치인지 vCmpData 변수가 가지고 있으니 AND 함수를 이용합니다
결과 : vCmpData AND vTo = (0, 50, 0, 0)
// 위에서 만든 to만 있는 값과
// from과 같지 않은 유지된 값을 더해서
// 최종값 생성
vData = _mm_add_ps(vData, vToData);
// 배열에 저장
_mm_storeu_ps(p + i, vData);
위의 2줄에서 다 구했으니 더하고 저장하기만 하면 되겠습니다.
위의 함수들로 끝나겠네요.
storeu 함수의 경우도 store 함수가 있지만 여기서는 storeu 함수를 사용했습니다.
결과를 확인해봅시다.
잘 나왔네요.ㅎㅎ
간단하게 and, andnot, cmp계열 함수들을 알아보았습니다.
다른 비교연산 함수들(or, xor)은 따로 다루지는 않겠습니다.
인텔 인트린식 가이드를 꼭 참고하시기 바랍니다.
* 추가하면 좋은 내용이나 부족한 부분은 댓글로 남겨주세요!
'SIMD' 카테고리의 다른 글
SIMD Programming 01 - 시작 및 자료형 (0) | 2019.10.04 |
---|