D 언어 사양: 벡터 확장

D 언어 사양: 벡터 확장 (SIMD)

CPU는 전용 벡터 타입(vector type)과 벡터 연산(vector operation)을 지원하는 경우가 많아요. 이때 말하는 벡터 연산은 일명 *미디어 명령어(media instructions)*라고도 불러요. 벡터 타입은 부동소수점 타입이나 정수 타입을 고정된 개수만큼 담는 배열이고, 벡터 연산은 그 요소들에 동시에 같은 작업을 적용해요.

이런 하드웨어 기능을 D에서 직접 쓰고 싶다면, 전용 Vector 타입이 그 접근을 제공해요. 이 타입의 VectorBaseType(VectorBaseType)은 반드시 Static Array(정적 배열)여야 하고요. VectorElementType은 그 정적 배열에서 한정자(qualifier)를 뗀 요소 타입이에요. 그리고 그 정적 배열의 크기(dimension)가 곧 벡터 안에 들어 있는 요소의 개수가 돼요.

구현 정의(Implementation Defined): 어떤 벡터 타입을 지원할지는 대상(target) 아키텍처에 달려 있어요. 구현체는 기본적으로 대상 하드웨어가 실제로 지원하는 벡터 타입과 연산만 지원할 것으로 기대돼요.

근거(Rationale): 지원하지 않는 벡터 타입이나 연산을 에뮬레이션으로 흉내 내면 성능이 너무 나빠질 수 있어요. 그럴 바에는 에뮬레이션에 기대기보다, 사용자가 다른 알고리즘을 고르는 편이 나을 정도로요.

모범 사례(Best Practices): 언어 문법의 Vector 문법 대신, core.simd 안에 있는 선언들을 쓰는 걸 권장해요.

본문

core.simd

벡터 타입과 벡터 연산은 core.simd를 임포트하면서 도입돼요.

import core.simd;

구현 정의: 이 타입들과 연산은 컴파일러가 타깃으로 삼는 아키텍처를 기준으로 정의된 것들이에요. 만약 특정 CPU 제품군이 벡터 타입 지원을 제각각이라면, 추가로 런타임 검사가 필요할 수 있어요. 그런데 컴파일러는 런타임 검사를 만들어 주지 않아요. 그 검사는 프로그래머가 직접 해야 해요.

구현 정의: 대상 아키텍처에 따라서는, SIMD 타입 지원을 활성화하기 위해 컴파일러 플래그가 필요할 수도 있어요.

정의되는 타입들은 모두 다음과 같은 네이밍 규칙을 따라요.

typeNN

여기서 type은 벡터 요소 타입이고, NN은 그 벡터 타입에 담기는 요소의 개수예요. 타입 이름은 키워드가 되지 않아요.

프로퍼티 (Properties)

벡터 타입은 다음과 같은 프로퍼티를 갖고 있어요.

프로퍼티 설명
.array 정적 배열 표현(static array representation)을 돌려줘요

벡터는 벡터 요소 타입에 따라 다음과 같은 프로퍼티들도 지원해요. 이 프로퍼티가 만들어 내는 값은, 같은 타입의 벡터인데 각 요소가 그 요소 타입의 프로퍼티 값으로 채워진 벡터예요.

정수 벡터 타입 프로퍼티

프로퍼티 설명
.min 최솟값
.max 최댓값

부동소수점 벡터 타입 프로퍼티

프로퍼티 설명
.epsilon 값 1에 더했을 때 가장 작은 증가분
.infinity 무한대 값
.max 무한대가 아닌 값 중 가장 큰, 표현 가능한 값
.min_normal 0이 아닌 값 중 가장 작은, 표현 가능한 값
.nan NaN 값

변환 (Conversions)

같은 크기(요소 개수 × 요소 크기, number_of_elements * size_of_element)를 가진 벡터 타입끼리는 서로 암시적으로 변환할 수 있어요. 이건 reinterpret cast, 다시 말해 타입 재해석(paint) 방식으로 이뤄져요. 벡터 타입은 자신의 VectorBaseType으로 캐스팅할 수도 있어요.

정수와 부동소수점 값은 자신의 벡터에 대응하는 타입으로 암시적으로 변환돼요.

int4 v = 7;
v = 3 + v;   // add 3 to each element in v

아까 int4 v = 7;처럼 스칼라 값 하나가 벡터로 들어가면, 그 값이 모든 요소에 똑같이 채워져요. 그래서 v = 3 + v;는 v의 각 요소에 3씩 더하는 결과가 되고요.

벡터 요소 하나씩 접근하기 (Accessing Individual Vector Elements)

벡터 요소는 직접 접근할 수는 없어요. 하지만 배열 타입으로 변환하면 접근할 수 있어요.

int4 v;
(cast(int*)&v)[3] = 2;   // set 3rd element of the 4 int vector
(cast(int[4])v)[3] = 2;  // set 3rd element of the 4 int vector
v.array[3] = 2;          // set 3rd element of the 4 int vector
v.ptr[3] = 2;            // set 3rd element of the 4 int vector

네 줄 모두 4개짜리 int 벡터의 세 번째 요소를 2로 설정하는 코드예요. 주소를 int 포인터로 캐스팅해서 접근하거나, int[4] 배열로 캐스팅하거나, .array.ptr 프로퍼티를 쓰는 식으로요.

조건부 컴파일 (Conditional Compilation)

벡터 확장이 구현되어 있으면 version 식별자 D_SIMD가 설정돼요.

어떤 타입이 존재하는지 아닌지는 컴파일 타임에 IsExpression으로 검사할 수 있어요.

static if (is(typeNN))
    ... yes, it is supported ...
else
    ... nope, use workaround ...

어떤 타입에 대해 특정 연산을 지원하는지도 컴파일 타임에 검사할 수 있어요.

float4 a,b;
static if (__traits(compiles, a+b))
    ... yes, add is supported for float4 ...
else
    ... nope, use workaround ...

특정 벡터 명령어를 런타임에 쓸 수 있는지 검사하고 싶다면, core.cpuid의 함수들을 쓰면 돼요.

지원하지 않는 벡터 연산에 대한 대표적인 우회 방법은, 배열 연산(array operations)을 대신 쓰는 거예요.

float4 a,b;
static if (__traits(compiles, a/b))
    c = a / b;
else
    c[] = a[] / b[];

X86 및 X86_64 벡터 확장 구현

구현 정의:

다음은 X86과 X86_64 아키텍처에서 벡터 타입이 구체적으로 어떻게 구현되는지 설명해요.

벡터 확장은 현재 OS X 32비트 타깃과 모든 64비트 타깃에서 구현되어 있어요.

core.simd는 다음 타입들을 정의해요.

타입 이름 설명 gcc 동등 표현(Equivalent)
void16 16바이트의 타입 없는 데이터 없음(no equivalent)
byte16 byte 16개 signed char __attribute__((vector_size(16)))
ubyte16 ubyte 16개 unsigned char __attribute__((vector_size(16)))
short8 short 8개 short __attribute__((vector_size(16)))
ushort8 ushort 8개 ushort __attribute__((vector_size(16)))
int4 int 4개 int __attribute__((vector_size(16)))
uint4 uint 4개 unsigned __attribute__((vector_size(16)))
long2 long 2개 long __attribute__((vector_size(16)))
ulong2 ulong 2개 unsigned long __attribute__((vector_size(16)))
float4 float 4개 float __attribute__((vector_size(16)))
double2 double 2개 double __attribute__((vector_size(16)))
void32 32바이트의 타입 없는 데이터 없음(no equivalent)
byte32 byte 32개 signed char __attribute__((vector_size(32)))
ubyte32 ubyte 32개 unsigned char __attribute__((vector_size(32)))
short16 short 16개 short __attribute__((vector_size(32)))
ushort16 ushort 16개 ushort __attribute__((vector_size(32)))
int8 int 8개 int __attribute__((vector_size(32)))
uint8 uint 8개 unsigned __attribute__((vector_size(32)))
long4 long 4개 long __attribute__((vector_size(32)))
ulong4 ulong 4개 unsigned long __attribute__((vector_size(32)))
float8 float 8개 float __attribute__((vector_size(32)))
double4 double 4개 double __attribute__((vector_size(32)))

참고: 32비트 gcc와 clang에서는 long 대신 long long이에요.

지원되는 128비트 벡터 연산자

연산자 void16 byte16 ubyte16 short8 ushort8 int4 uint4 long2 ulong2 float4 double2
= × × × × × × × × × × ×
+ × × × × × × × × × ×
- × × × × × × × × × ×
* × × × ×
/ × ×
& × × × × × × × ×
| × × × × × × × ×
^ × × × × × × × ×
+= × × × × × × × × × ×
-= × × × × × × × × × ×
*= × × × ×
/= × ×
&= × × × × × × × ×
|= × × × × × × × ×
^= × × × × × × × ×
== × × × × × × × × × ×
!= × × × × × × × × × ×
< × × × × × × × × × ×
<= × × × × × × × × × ×
>= × × × × × × × × × ×
> × × × × × × × × × ×
단항 ~ × × × × × × × ×
단항 + × × × × × × × × × ×
단항 - × × × × × × × × × ×

지원되는 256비트 벡터 연산자

연산자 void32 byte32 ubyte32 short16 ushort16 int8 uint8 long4 ulong4 float8 double4
= × × × × × × × × × × ×
+ × × × × × × × × × ×
- × × × × × × × × × ×
* × ×
/ × ×
& × × × × × × × ×
| × × × × × × × ×
^ × × × × × × × ×
+= × × × × × × × × × ×
-= × × × × × × × × × ×
*= × ×
/= × ×
&= × × × × × × × ×
|= × × × × × × × ×
^= × × × × × × × ×
== × × × × × × × × × ×
!= × × × × × × × × × ×
< × × × × × × × × × ×
<= × × × × × × × × × ×
>= × × × × × × × × × ×
> × × × × × × × × × ×
단항 ~ × × × × × × × ×
단항 + × × × × × × × × × ×
단항 - × × × × × × × × × ×

표에 나오지 않은 연산자는 아예 지원되지 않아요. (위 표에서 ×는 지원됨, –는 지원되지 않음을 뜻해요.)

벡터 연산 내장 함수 (Vector Operation Intrinsics)

지원되는 내장 함수(intrinsics)에 대해서는 core.simd를 참고해요.

더 알아보기 (Learn more)