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를 참고해요.