1. Introduction
Arm Neon intrinsic은 assembler 보다 쉽게 유지 관리되는 NEON 코드를 작성하는 방법이다.
Neon intrinsic은 컴파일러가 적절한 Neon 명령어 또는 일련의 Neon 명령어로 대체하는 functional call이다.
Neon 레지스터는 D레지스터와 Q레지스터 총 2개로 이루어지는데, D레지스터는 64-bit 데이터를 처리하고, Q레지스터는 128-bit 데이터를 처리한다.
Neon 레지스터에 직접 매핑되는 C변수를 만들면 이 변수는 Neon Instrinsic에 전달되고 컴파일러는 실제 서브루틴 호출을 수행하는 대신 Neon 명령어를 직접 생성한다.
Intrinsic은 어셈블리 만큼 많은 제어 기능을 제공하지만 레지스터 할당은 컴파일러에게 맡기므로 사용자는 보다 알고리즘에 집중할 수 있다. 또한 컴파일러는 C 또는 C++ 코드를 통해 intrinsic을 최적화하여 보다 효율적으로 사용 가능하다.
Neon Intrinsic은 arm_neon.h
헤더파일에 정의되어 있으며 헤더파일은 여러 벡터 타입을 정의하고 있다.
Armv7 이전 아키텍처에서는 Neon 명령어를 지원하지 않는다.
2. Vector Data Type
우선 벡터 데이터 타입에 대해 알아보자.
Neon vector data type은 x_t 패턴으로 이름이 지정된다.
int16x4_t
: 4개의 16비트 short int 값으로 구성된 벡터float32x4_t
: 4개의 32비트 float 값으로 구성된 벡터
데이터 타입은 D, Q 레지스터 별로 12개가 존재한다.
각 데이터 타입의 a x b = 64 or 128가 된다. ( a 비트 data가 b개 존재 )
64-bit (D레지스터) | 128-bt (Q레지스터) |
---|---|
int8x8_t | int8x16_t |
int16x4_t | int16x8_t |
int32x2_t | int32x4_t |
int64x1_t | int64x2_t |
uint8x8_t | uint8x16_t |
uint16x4_t | uint16x8_t |
uint32x2_t | uint32x4_t |
uint64x1_t | uint64x2_t |
float16x4_t | float16x8_t |
float32x2_t | float32x4_t |
poly8x8_t | poly8x16_t |
poly16x4_t | poly16x8_t |
이러한 벡터 데이터 유형 중 하나를 사용하여 intrinsic의 입력 및 출력을 지정할 수 있다.
일부 intrinsic은 벡터 type의 배열을 사용한다. 동일한 vector type을 2-4개 결합한다.
<type><size><number_of_lnaes>x<length_of_array>_t
여기서 type은 val 라는 single element를 포함하는 c structure이다.
이 type은 최대 4개의 레지스터를 load/store 할 수 있는 레지스터를 아래 예시와 같이 매핑해줄 수 있다.
struct int16x4x2_t
{
int16x4_t val[2];
} <var_name>;
이러한 type은 loads, stores, transpose, interleave, de-interleave 명령어에만 사용된다.
실제 데이터 연산을 하기 위해 개별 레지스터를 <var_name>.val[0], <var_name>.val[1] 처럼 선택해줄 수 있다.
이 array type은 길이가 2,3,4를 가지며 위 테이블에 있는 벡터들을 통해 만들수 있다.
이때, 벡터 데이터 타입은 문자 그대로 할당하여 초기화 할수 없으며
load intrinsic 또는 `vcreate`intrinsic을 사용하여 초기화 해주어야 한다.
3. Prototype of NEON Intrinsics
Intrinsic은 NEON unfied assembler 구문과 유사한 명명 체계를 사용한다.
<opname><flags>_<type>
128-bit 벡터에서 작동하도록 지정하기위해 q 플래그를 사용한다.
- `vmul_s16` : signed 16-bit values를 갖는 벡터를 곱한다.
이 명령어는 `VMUL.I16 d2, d0, d1` 으로 컴파일된다.
- `vaddl_u8` : unsigned 8-bit values를 포함하는 두개의 64-bit 벡터(long)를 더하여 unsigned 16-bit value를 갖는 128-bit 벡터가 된다.
이 명령어는 `VADDL.U8 q1, d0, d1`으로 컴파일된다.
이 때 컴파일러는 명령어를 일부 변경하여 최적화를 수행할 수도 있음에 유의하자.
__fp16 형식을 사용하는 Neon intrinsic 함수의 프로토타입은 타겟 디바이스 상에서 Neon half-precision VFP extension이 있을 때만 사용가능함에 유의하자.
__fp16을 사용하려면 --fp16_format command-line option을 사용하면된다.
4. Using Neon Intrinsics
ARM Complier toolchain은 Neon intrinsics를 arm_neon.h 파일에 정의한다.
Instrinsics은 ARM ABI의 일부분으로 ARM Compiler와 GCC 사이에서 portable하다.
`q` suffix가 붙으면 주로 Q 레지스터를 사용하고, 그렇지 않으면 D 레지스터를 사용한다고 했다.
(일부는 q가 없지만 Q 레지스터를 사용하기도 함.)
neon intrinsic 사용 예제를 살펴보자.
uint8x8_t vadd_u8(uint8x8_t a, uint8x8_t b);
위 예시는 unsigned 8-bit integer values를 8개 갖는 vector a와 b를 더하는 명령어를 의미한다.
vadd_u8은 q suffix가 붙지 않았으니 D 레지스터를 사용하며 이 경우 입력과 출력은 64-bit 벡터이다.
uint8x16_t vaddq_u8(uint8x16_t a, uint8x16_t b);
위 예시는 unsigned 8-bit integer values를 16개 갖는 vector a와 b를 더하는 명령어를 의미한다.
vaddq_u8은 q suffix가 붙었으니 Q 레지스터를 사용하며 이 경우 입력과 출력은 128-bit 벡터이다.
일부 Neon intrisics은 32-bit ARM 범용 레지스터를 입력 인자로 사용하여 스칼라 값을 지정한다.
- `vget_lane_u8` : 벡터에서 하나의 값을 추출
- `vset_lane_u8` : 벡터에 하나의 single lane을 설정
- `vcreate_u8` : literal value로 부터 벡터를 생성
- `vdup_n_u8` : 같은 literal value로 모든 lane을 설정
각 type에 대해 별도의 intrinsic을 사용한다는 것은 컴파일러가 어떤 type이 어떤 레지스터에 있는지 추적하기 때문에 호환되지 않는 type에 대해 연산을 수행하기 어렵다는 것을 의미한다.
또한 컴파일러는 program flow를 재조정하고 더 빠른 대체 명령어를 사용할 수 있다.
생성된 명령어가 intrisic 명령어에 내포된 명령어와 일치된다는 보장은 없다.
이 덕분에 한 마이크로 아키텍처에서 다른 마이크로 아키텍처로 이동할 때 특히 용이하다.
Ex 4.1
#include <arm_neon.h>
uint32x4_t double_elements(uint32x4_t input)
{
return (vaddq_u32(input, input));
}
위 예제는 32-bit unsigned integer의 4레인 벡터를 입력 매개변수로 사용하고 모든 레인의 값이 두 배가 된 벡터를 반환하는 함수이다.
Ex 4.2
double_elements PROC
VADD.I32 q0, q0, q0
BX lr
ENDP
위 예제는 hard float ABI용으로 컴파일 된 Ex 4.1에서 생성된 코드의 dis-assemble 된 버전을 보여준다.
이 double_elements() 함수는 single Neon 명령어와 반환 return sequence로 변환된다.
Ex 4.3
double_elements PROC
VMOV d0, r0, r1
VMOV d1, r2, r3
VADD.I32 q0, q0, q0
VMOV r0, r1, d0
VMOV r2, r3, d1
BX lr
ENDP
위 예제는 소프트웨어 연결을 위해 컴파일된 Ex 4.1의 dis-assemble이다.
이 상황에서 코드는 사용전 ARM 범용 레지스터에서 NEON 레지스터로 매개변수를 복사해야 한다.
계산 후 return 값을 NEON 레지스터에서 ARM 범용 레지스터로 다시 복사해야 한다.
GCC와 armcc는 동일한 intrinsic function을 지원하므로 Neon intrinsic 함수로 작성된 코드는 tool chain간에 완벽히 이식 가능하다. 이 때, intrinsics을 사용하는 모든 소스 파일에 arm_neon.h 헤더 파일을 포함해야 하며 command line option을 지정해주어야 한다.
Reference
https://developer.arm.com/documentation/den0018/a/NEON-Intrinsics?lang=en