Float-Point Dot Product in SSE4

이 글은 SSE41에 추가된 부동소수점 내적 연산 명령(instruction)에 대하여 소개한다. 내적 연산은 과학 계산, 멀티미디어, 게임 등에서 가장 많이 사용되는 연산 중 하나이며 SSE4에서 명령이 추가되었다. SSE4에는 SSE4.1, SSE4.2 서브셋(subset)과 SSE4a 확장이 있지만, 여기서는 SSE4라 부르며 실제로는 SSE4.1을 의미한다. SSE4.1은 Intel의 Core 마이크로아키텍처, AMD의 K10 마이크로아키텍처부터 적용되었다.2

여기서 설명할 DPPD와 DPPS 명령은 SSE4에서 추가된 부동소수점 내적 연산 명령으로, 여러 부동소수점 연산이 복합된 최초의 부동소수점 SSE 명령이다.3

DPPD는 Dot Product of Packed Double Precision Float-Point Values라는 의미로 배정밀도 부동소수점의 내적 연산 명령이고, DPPS는 단정밀도(single precision) 부동소수점 연산에 대응한다. 명령의 정의는 다음과 같다.

DPPD xmm1, xmm2/m128, imm8
DPPS xmm1, xmm2/m128, imm8

xmm은 128비트 SSE 레지스터, m128은 128비트 값을 가리키는 메모리 주소, imm8은 8비트 값을 의미한다. 따라서, DPPD, DPPS 명령은 첫번째 오퍼랜드로 레지스터, 두번째 오퍼랜드로 레지스터 또는 주소, 마지막 오퍼랜드로 8비트 값을 입력받는다. 조금 더 친숙한 정의로 바꿔보면 다음과 같다.

DPPD dst, src, mask
DPPS dst, src, mask

src와 dst에 해당하는 128비트 레지스터의 값 또는 주소가 가리키는 곳의 128비트 값은 배정밀도 부동소수점(64비트) 값이 2개 또는 단정밀도 부동소수점(32비트) 값이 4개가 묶여있도록(packed) 준비되어 있어야 한다. 즉, 이 명령은 배정밀도 부동소수점은 2차원 벡터, 단정밀도 부동소수점은 4차원 벡터까지 한번에 내적할 수 있다. 연산은 src와 dst가 내적 연산을 수행한 후 dst에 결과가 저장된다. 이 때, mask 값을 통하여 내적 연산이 어떻게 일어날 것인지 선택가능하다. 단정밀도 부동소수점을 예로 들어 src와 dst에 x, y, z, w의 원소들이 묶여있다고 할 때, mask 값의 상위 4비트는 x, y, z, w 중 어느 원소들을 내적 연산에 사용할 것인지를 결정한다. 만약 x, y의 2차원 벡터를 내적 연산할 경우에 30h이 되고, x, y, z, w의 4차원 벡터를 내적 연산할 경우에는 F0h이 된다. 하위 4비트는 내적 연산의 결과가 128비트 레지스터의 어느 원소에 저장될 것인지를 결정한다. 즉, 01h이 되면 x원소에 저장되고, 08h이 되면 w원소에 저장된다. 이것은 4x4 행렬을 곱할 때처럼 연속적인 내적 연산을 할 때 유용하게 사용된다.

최근에는 어셈블리어를 사용하지 않고, 아키텍처에 구애받지 않으면서 컴파일러에게 더 많은 최적화의 기회를 줄 수 있는 intrinsic 함수를 사용하는 경우가 대부분으로 그 정의는 다음과 같다.

#include <smmintrin.h>

__m128d _mm_dp_pd(__m128d a, __m128d b, const int mask);
__m128  _mm_dp_ps(__m128  a, __m128  b, const int mask);

* 여기서 __m128d, __m128은 위의 m128과는 관련없이 즉, 메모리 주소가 아니라 묶여진(packed) 128비트 데이터를 의미한다.

이 함수들이 내부적으로 어떻게 작동하는지 살펴보자. _mm_dp_ps 함수를 사용하는 C언어 코드를 디스어셈블한 결과는 아래와 같다.

; Microsoft Visual C++ 10
movaps xmm0, xmmword ptr [esp+eax+1020h] ; (1)
movaps xmm1, xmmword ptr [esp+eax+5020h] ; (2)
dpps   xmm1, xmm0, 0F1h                  ; (3)
movaps xmmword ptr [esp+10h], xmm1       ; (4)

(1)에서는 전달받은 a를 (그것의 주소로 부터) 128비트 SSE 레지스터 XMM0에 적재(load)하고, (2)에서는 b를 XMM1에 적재한다. (3)에서 XMM1과 XMM0 레지스터의 내용이 F1h 마스크값에 따라 내적 연산이 수행되고 결과는 다시 XMM1에 저장된다. (4)에서 XMM1 레지스터의 값을 결과가 저장될 메모리에 저장(store)한다. 또한, __m128 값은 다음과 같은 구조체로 C언어에서 쉽게 적용할 수 있다.

__declspec(align(16)) /* 값은 16바이트로 정렬된 주소에 존재해야 한다. */
struct Vector4f {
    union {
        struct {
            /* 일반적인 상황에서 이 변수들을 사용한다. */
            float x, y, z, w;
        };

        /* intrinsic 함수에 __m128 타입으로 필요할 때 이 변수를 사용한다. */
        __m128 packed;
    };
};

이 명령으로 단정밀도 부동소수점 4차원 벡터의 내적에 필요한 명령인 곱셈 4번과 덧셈 3번을 하나의 명령(instruction)으로 줄일 수 있고, 이것을 활용하면 4x4 행렬끼리의 곱에 필요한 명령인 곱셈 64번과 덧셈 48번을 16개의 명령으로 줄일 수 있다.