일단 예시부터 놓을 거지만 밑에 설명부터 읽어보고 다시 보기


// 스칼라를 생성하여 리턴 // 하지만 나중에 벡터로 변환해서 다른 벡터와 계산할 필요성 존재 // 이로 인해 불필요한 레지스터에의 load 및 메모리에의 store로 발생 // 직관적이지만 느려서 SIMD를 쓴 보람이 없으며, 심지어 안 쓴 것보다 느릴 가능성 존재 float dot_bad(float32x4_t const& va, float32x4_t const& vb) { // 두 벡터 곱한 결과 벡터 // (x0*x1, y0*y1, z0*z1, w0*w1) auto const vt = vmulq_f32(va, vb); // 결과 벡터의 각 요소를 다 더한 스칼라 => 메모리에 store // x0*x1 + y0*y1 + z0*z1 + w0*w1 return vaddvq_f32(vt); } // 스칼라 생성 배제하면서도 벡터내 요소들의 합을 구하기 위해 벡터들을 지지고 볶는 똥꼬쇼 필요 // 연산 개수가 많아보이지만 계속 레지스터 내에서 일어나는 일이라 우수한 성능 // 리턴된 레지스터 내 벡터는 바로 다른 벡터와 연산하여 불필요한 load/store 방지 // 예를 들면 벡터 정규화 등에 사용 float32x4_t dot_good(float32x4_t const& va, float32x4_t const& vb) { // 앞 예시에서 처럼 일단 두 벡터 곱하기 // 그러나 이것을 바로 더하지는 않는다 // (x0*x1, y0*y1, z0*z1, w0*w1) float32x4_t const multiplied = vmulq_f32(va, vb); // 이제부터는 벡터를 뒤집고 더하고 자르고 순서 바꿔 붙이는 등의 짜증나는 과정 // (w0*w1, z0*z1, y0*y1,x0*x1) float32x4_t const multiplied_reversed = vrev64q_f32(multiplied); // ( // x0*x1 + w0*w1, // y0*y1 + z0*z1, // y0*y1 + z0*z1, // x0*x1 + w0*w1, // ) float32x4_t const t1 = vaddq_f32(multiplied, multiplied_reversed); // (y0*y1 + z0*z1, x0*x1 + w0*w1) float32x2_t const t1_high = vget_high_f32(t1); // (x0*x1 + w0*w1, y0*y1 + z0*z1) float32x2_t const t1_low = vget_low_f32(t1); // ( // y0*y1 + z0*z1, // x0*x1 + w0*w1, // x0*x1 + w0*w1, // y0*y1 + z0*z1, // ) float32x4_t const t2 = vcombine_f32(t1_high, t1_low); // 결과적으로 벡터 내의 요소가 동일하게 초반 벡터곱 결과의 요소들의 합들로 채워짐 // ( // x0*x1 + y0*y1 + z0*z1 + w0*w1, // x0*x1 + y0*y1 + z0*z1 + w0*w1, // x0*x1 + y0*y1 + z0*z1 + w0*w1, // x0*x1 + y0*y1 + z0*z1 + w0*w1, // ) // (dot, dot, dot, dot) return vaddq_f32(t1, t2); }


(예시 설명 추가 : 이 함수들 자체는 bad가 good보다 빠름 다만 이 함수를 전체 연산의 과정 중 하나로 생각해야하기 때문에 good 방식으로 작성해야 한다는 것)



SIMD(단일 명령어 다중 데이터)는 연산의 컨셉을 이해하는게 제일 중요하다


1. 메모리에서 SIMD용 레지스터로 벡터 형태의 데이터를 로드한다 (벡터의 크기는 기본적으로는 레지스터의 크기 이하여야함, 다만 1개의 큰 벡터를 여러개의 레지스터에 저장해서 처리하는 방식도 존재)

2. 레지스터에 존재하는 벡터들간의 연산들을 한다 (벡터 하나에는 여러개의 데이터가 있으므로 명령 한번에 동일 종류의 여러개의 연산이 이루어지는 셈)

3. 최종 결과를 다시 메모리에 store한다


그런데 안타깝게도 연산에 비해 load/store는 겁나 비용이 크다

이 때문에 단 한번의 벡터 연산을 위해 이 과정을 거치면 SIMD를 안 쓰는 것보다 느릴 수도 있다

결과적으로 SIMD 최적화의 핵심은 한번 로드한 데이터를 최대한 레지스터 내에서 반복해 가공하다가 최후의 결과값만 메모리에 스토어하는 것


물론 벡터의 형태를 유지하면서 연산을 반복한다는 건 꽤 신경을 써야하는 일

예시 코드처럼 곱한다음에 다 더한다는 이 간단한 일을 굳이 벡터들로만 하기 위해 다양한 변형과정을 거쳐야 함

(벡터 내의 특정 요소 값만 써야할 때도 있는데 로드/스토어 없이 바로 다른 벡터와의 연산에 쓸 수 있도록 하는 인스트럭션이 존재하기는 한다)

여기에는 노하우가 필요하기 때문에 상용 코드들 구경도 열심히 하고 벤치마킹 하는 습관도 들여야 함

(참고로 코드의 관련 함수와 실제 실행되는 CPU 인스트럭션은 1:1로 매칭되지 않기 때문에 어셈도 봐둘 필요가 있음 옛날에는 SIMD 파트 만큼은 직접 어셈으로 작성하는 경우가 많았음)


물론 대부분은 직접 쓰기 보다는 맛만 보다가 라이브러리의 형태로 사용할 것이므로 걍 그런가 보다 하고 넘어가면 된다

굳이 직접 작성해야한다면 신경 많이 써야 최소한 손해는 안보는 영역이라는게 이 글의 요지임



지금은 삭제된 며칠 전 질문글 예시에 이러한 점들을 간과한게 많이 보여서 씀