SIMD
Table of Contents
1. SIMD
1.1. NEON
1.1.1. 指令格式
1.1.1.1. overview
v{1.q}{2.r}{3.d}{13.p}{11.h} \ XXX \ {17.n|m|p|a|i|x}{16.u|s}{4.l}{5.w}{12.hn}{6.q}{14.nm}{15.v}{7.b|s|h|d}_{8.lane{q}}_{9.high}_{10.n}_type
- 1.q saturating
- 2.r rounding
- 3.d doubling
- 4.l widen
- 5.w 只 widen 一个输入参数
- 6.q long (128-bit)
- 7.b 参数都是 scalar: int8,int16,int32,int64
- 8.lane, 使用 v[lane] 而不是 v[i]
- 9.high, 使用 x[i+x] 而不是 x[i]
- 10.n 有一个参数是 scalar
- 11.h half narrow, 例如 xx>>1
- 12.hn high narrow, 例如 xx>>8
- 13.p pairwise
- 14.nm max 类指令使用 nm 表示符合 ieee754
- 15.v across vector
- 16.u unsigned, 输入是 signed, 输出变成 unsigned
- 16.s signed, 输入是 unsigned, 输出变成 signed
- 17.n round to nearest even
- 17.m rount to minus inf
- 17.p round to plus inf
- 17.a round to nearest away
1.1.1.2. saturating
某些会 overflow 的计算时会有 saturating 的版本, 用开头的 `q` 表示, 例如:
int8x8_t v[q]add_s8(int8x8_t a,int8x8_t b)
, 加法int8x8_t v[q]movn_s16(int16x8_t a)
, int16 转换为 int8int8x8_t v[q]shl_s8(int8x8_t a,int8x8_t b)
, 左移int8x8_t v[q]abs_s8(int8x8_t a)
, 当 a 为 INT8_MIN 时计算会溢出
1.1.1.3. rounding
整数的右移计算需要舍入, 默认的舍入方法是 minus inf, rounding 表示需要变成 plus inf. 例如:
- 无 rounding 时, 7>>2=3, -7>>2=-4
- 有 rounding 时, 7>>2=4, -7>>2=-3
通过开头加 `r` 表示需要 roudning, 例如:
int8x8_t v[r]shl_s8(int8x8_t a,int8x8_t b)
, 相当于(a+(1<<(n-1)))>>b
int8x8_t v[r]hadd_s8(int8x8_t a,int8x8_t b)
, 因为 hadd 相当于(a+b)>>1
, 所以也有 rounding 的问题
除了整数右移, vrnd
和 vcvt
指令支持指定不同的 rounding 方法, NEON 支持的
rounding 方法主要有:
round toward zero
默认的方法, 也是 c 语言中 float 转型成 int 时使用的方法, 表示 round 到最接近 0 的值, 例如 (int)3.5=3, (int)(-3.5)=-3
neon 指令没有任何前缀 (n,m,p,a) 时表示使用这种 rounding 方法, 例如:
int32x2_t vcvt_s32_f32(float32x2_t a)
round to nearest with ties to even
round 到离它最近的偶数, 这是 IEEE754 默认的方法, 在指令时使用 `n` 前缀表示, 例如:
int32x2_t vcvt[n]_s32_f32(float32x2_t a)
float32x2_t vrnd[n]_f32(float32x2_t a)
round toward minus infinity
相当于 floor, 在指令时使用 `m` 前缀表示, 例如:
float32x2_t vrnd[m]_f32(float32x2_t a)
int32x2_t vcvt[m]_s32_f32(float32x2_t a)
round toward plus infinity
相当于 ceil, 在指令时使用 `p` 前缀表示, 例如:
float32x2_t vrnd[p]_f32(float32x2_t a)
int32x2_t vcvt[p]_s32_f32(float32x2_t a)
round to nearest with ties to away
四舍五入, 在指令时使用 `a` 前缀表示, 例如:
float32x2_t vrnd[a]_f32(float32x2_t a)
int32x2_t vcvt[a]_s32_f32(float32x2_t a)
不同 rounding 方法的结果如下所示:
11.5 | 12.5 | −11.5 | −12.5 | |
---|---|---|---|---|
to nearest, ties to even | 12 | 12 | −12.0 | −12.0 |
to nearest, ties away from zero | 12 | 13 | −12.0 | −13.0 |
toward 0 | 11 | 12 | −11.0 | −12.0 |
toward +inf | 12 | 13 | −11.0 | −12.0 |
toward −inf | 11 | 12 | −12.0 | −13.0 |
1.1.1.4. vector width
默认使用 64-bit vector, 例如 int8x8_t vqadd_s8(int8x8_t a,int8x8_t b)
使用
64-bit vector, 所以只能同时操作 8 个 int8_t
, 通过 `q` 后缀, 表示使用 128-bit
vector, 例如: int8x16_t vadd[q]_s8(int8x16_t a,int8x16_t b)
1.1.1.5. widen
通常情况下 NEON 的输入和输出元素的宽度是相同的, 例如 int8x8_t vqadd_s8(int8x8_t
a,int8x8_t b)
的输入和输出都是 int8_t
. widen 是指把输出元素的宽度变成输出的两倍, 比如输入是 int8_t
, 输出变成 int16_t
. 用 `l` 后缀表示 widen, 例如:
int16x8_t vadd[l]_s8(int8x8_t a,int8x8_t b)
1.1.1.6. narrow
narrow 与 widen 相反, 输出元素的宽度变成输入的一半, 用 `n` 后缀表示, 例如:
int8x8_t vmov[n]_s16(int16x8_t a)
uint8x8_t vqshru[n]_n_s16(int16x8_t a,const int n)
另外, add 指令有特殊的 narrow 方法:
int8x8_t vadd[hn]_s16(int16x8_t a,int16x8_t b)
, 其中的 `hn` 表示 high narrow,
因为它是取的输入元素的 `high part` 做为 narrow 的结果: r[i]=(a[i]+b[i])>>8
1.1.1.7. high/low
有些指令使用 high/low 后缀, 表示只操作输入或输出的一半元素, 例如:
int8x16_t vmovn_[high]_s16(int8x8_t r,int16x8_t a)
表示输出的后一半元素来自 a
int16x8_t vmovl_[high]_s8(int8x16_t a)
表示 a 只有后一半元素会被 mov 到输出
int16x8_t vshll_[high]_n_s8(int8x16_t a,const int n)
表示 a 的后一半元素会被
shift 后保存到输出中 (a 有 16 个 元素, 但结果只有 8 个元素)
1.1.1.8. scalar
1.1.1.8.1. lane
使用 lane 后缀表示输入会使用某个 vector 的某一个固定的元素, 例如
int16x4_t vmla_[lane]_s16(int16x4_t a,int16x4_t b,int16x4_t v,const int lane)
表示 r[i]=a[i]+b[i]*v[lane]
, 即对于 v 总是使用 lane 位置的元素而忽略其它元素
1.1.1.8.2. n
使用 n 后缀表示有输入是一个 int scalar, 例如: int8x8_t vsli_[n]_s8(int8x8_t
a,int8x8_t b,const int n)
1.1.1.8.3. b/h/s/d
这几个后缀表示输入是标量而不是向量.
当操作 int 时:
- b 代表 int8_t
- h 代表 HI, 即 int16_t
- s 代表 SI, 即 int32_t
- d 代表 DI, 即 int64_t
当操作 float 时:
- s 代表 single precision, 即 float
- d 代表 double precision, 即 double
1.1.1.9. pairwise
以 pairwise add 为例, 它把两个输入拼接在一起, 计算的结果是 r[i]=a[2i]+a[2i+1]
,
使用 `p` 前缀表示 pariwise, 例如 int8x8_t v[p]add_s8(int8x8_t a,int8x8_t b)
1.1.1.10. across vector
across vector 相当于 reduction, 使用 `v` 后缀表示, 例如 int8_t
vaddv_s8(int8x8_t a)
, 它计算的是 vector 的 sum
1.1.1.11. unsigned
有些指令会把输入的 signed int 变成输出的 unsigned int, 使用 `u` 后缀表示, 例如
uint8x8_t vqmov[u]n_s16(int16x8_t a)
1.2. MSA
1.2.1. 指令格式
{7.h}{4.a}xxx{8.r}{1.v}{5.s}{6.i}_{2.s,u}_{3.b,h,w,d}
- 1.v
- 2.s,u signed/unsigned
- 3.b,h,w,d 向量元素的类型: int8, int16, int32, int64
- 4.a absolute
- 5.s saturate
- 6.i 立即数
- 7.h horizontal
- 8.r rounding
1.3. NEON vs.MSA
https://github.com/sunwayforever/hello_world/tree/main/hello_neon/neon_msa
使用 msa 实现 neon 的基本操作时主要有以下几个问题:
- neon 支持 64/128 bit vector, msa 只支持 128 bit, 导致 msa 实现类似于 vadd_s8 的指令时会浪费一半的计算
- msa 不支持 neon 的 widen/narrow 类型的指令, 例如
int16x8_t vaddl_s8(int8x8_t a, int8x8_t b)
, 需要额外的代码把 msa 的输入变成int16x8_t
, 而 msa 又不支持用于数据类型转换的vmov
指令, 导致转换很低效. - msa 不支持 neon 的 signed/unsigned 类型指令, 例如
int8x8_t vuqadd_s8(int8x8_t a,uint8x8_t b)
, 因为 msa 的输入输出类型基本上总是相同的, 需要手动做额外的数据转换 - msa 不支持 neon 的 high 类型的指令, 例如
int16x8_t vaddl_high_s8(int8x16_t a, int8x16_t b)
, 需要手动从int8x16_t
构造int16x8_t
- msa 不支持 cross vector 类型的指令, 例如
int8_t vaddv_s8(int8x8_t a)
- 当涉及到 neon 的 pairwise 操作时(例如
int16x4_t vpadal_s8(int16x4_t a,int8x8_t b)
) 且 neon 是 64-bit vector 时, msa 会因为 vector 后一半填充的 0 导致 pairwise 结果错误. - msa 不支持 lane 指令和 scalar 指令
- msa 的 left shift 与 neon 的定义差异太大, 无法支持
1.4. SIMDe
1.4.1. overview
SIMDe (SIMD everywhere) 让使用不同的 SIMD (sse, neon, msa, …) 编写的代码可以运行在非 native 的平台上, 例如 sse 运行在 arm, 或者 neon 运行在 x86, …
它的基本做法是:
void my_sse_add() { #ifdefne _sse_ _sse_add(); #elif defined _neon_ /* 使用 neon 实现 */ #elif defined _msa_ /* 使用 msa 实现 */ #elif defined _vector_extension_ /* 使用 vector extension */ r = a + b; #else #pragma omp simd /* 使用 openmp simd */ for (int i = 0; i < N; i++) { r[i] = a[i] + b[i]; } #endif } #define _sse_add my_sse_add
- 如果当前 target 支持 sse, 可以直接调用 sse intrinsic
- 如果 target 支持 neon, 可以尝试用 neon 间接实现 sse 的功能
- 如果当前编译器支持 vector extension, 可以使用 vector extension 来利用 target 上的 SIMD
- 最后尝试使用 openmp 的 simd 支持 (和自动向量化效果差不多)
1.4.2. 实现细节
1.4.2.1. vector extension
参考这个例子: https://godbolt.org/z/zesG3sxbs
vector extension 依赖于编译器后端是否有 simd 支持 (例如 md 是否支持 V4SI machine mode), 如果不支持, 则 pass_lower_vector 会把 vector 操作变成 scalar 操作
当 vector extension 指定了超过 SIMD 长度的 vector_size 时, 编译器会把它拆分成多个 vector 操作
1.4.2.2. 宏
SIMDe 通过宏 (编译器预定义或用户定义) 控制生成的代码, 例如:
- SIMDE_ARM_NEON_A32V7_NATIVE
- SIMDE_X86_SSE2_NATIVE
- SIMDE_VECTOR_SUBSCRIPT
这几个宏是根据编译器的预定义宏自动设置的, 表示是否支持 neon/sse2, 是否支持 vector extension
1.4.2.3. 数据类型
以 vaba_s32 为例, 它操作的数据类型是 `4 个 int32 构成的 vector`, SIMDe 定义了两种相关的类型:
- simde_int32x4_t
- simde_int32x4_private
1.4.2.3.1. simde_int32x4_t
simde_int32x4_t 是 native simd (sse, neon, …) 数据类型的别名, 它会做为 simde_xxx 函数的输入输出以便与 native simd intrinsic 函数一致
#if defined(SIMDE_ARM_NEON_A32V7_NATIVE) /* ... */ typedef int32x4_t simde_int32x4_t; /* ... */ #elif defined(SIMDE_X86_SSE2_NATIVE) /* ... */ typedef __m128i simde_int32x4_t; /* ... */ #endif
1.4.2.3.2. simde_int32x4_private
typedef union { #if defined(SIMDE_VECTOR_SUBSCRIPT) int32_t values __attribute__((vector_size 16)); #else int32_t values[4]; #endif #if defined(SIMDE_X86_SSE2_NATIVE) __m128i m128i; #endif #if defined(SIMDE_ARM_NEON_A32V7_NATIVE) int32x4_t neon; #endif } simde_int32x4_private;
simde_vaba_s32 的代码会用许多宏来区分不同的实现 (x86/neon native, vector extension, omp simd), 每种实现会使用不同的数据类型. 通过 simde_int32x4_private 可以避免 native simd 类型与这些类型之间转换. 另外,不借助 union 无法把 int32x4_t 直接转换成 int32_t[4]
simde_int32x4_private 在 simde_xxx 函数的内部使用
1.4.2.3.3. simde_int32x4_to_private/simde_int32x4_from_private
这两个函数的定义为:
static simde_int32x4_private simde_int32x4_to_private(simde_int32x4_t value) { simde_int32x4_private r; simde_memcpy(&r, &value, sizeof(r)); return r; } static simde_int32x4_t simde_int32x4_from_private(simde_int32x4_private value) { simde_int32x4_t r; simde_memcpy(&r, &value, sizeof(r)); return r; }
实际上 gcc 的 pass_fre (Full Redundancy Elimination) 会识别到 r 是 value 的别名, 会把这个 memcpy 去掉, 最终相当于一个类型转换
1.4.2.4. example
以 neon 的 vaba_s32 为例:
/* NOTE: vaba_s32(a,b,c) 执行的操作是 `a += abs(b-c)` */ simde_int32x4_t simde_vabaq_s32( simde_int32x4_t a, simde_int32x4_t b, simde_int32x4_t c) { #if defined(SIMDE_ARM_NEON_A32V7_NATIVE) return vabaq_s32(a, b, c); #else return simde_vaddq_s32(simde_vabdq_s32(b, c), a); #endif } #if defined(SIMDE_ARM_NEON_A32V7_ENABLE_NATIVE_ALIASES) #undef vabaq_s32 #define vabaq_s32(a, b, c) simde_vabaq_s32((a), (b), (c)) #endif /* ======================================================= */ simde_int32x4_t simde_vabdq_s32(simde_int32x4_t a, simde_int32x4_t b) { #if defined(SIMDE_ARM_NEON_A32V7_NATIVE) return vabdq_s32(a, b); #elif defined(SIMDE_POWER_ALTIVEC_P6_NATIVE) return vec_sub(vec_max(a, b), vec_min(a, b)); #else simde_int32x4_private r_, a_ = simde_int32x4_to_private(a), b_ = simde_int32x4_to_private(b); #if defined(SIMDE_X86_SSE4_1_NATIVE) r_.m128i = _mm_sub_epi32( _mm_max_epi32(a_.m128i, b_.m128i), _mm_min_epi32(a_.m128i, b_.m128i)); #elif defined(SIMDE_X86_SSE2_NATIVE) const __m128i m = _mm_cmpgt_epi32(b_.m128i, a_.m128i); r_.m128i = _mm_xor_si128(_mm_add_epi32(_mm_sub_epi32(a_.m128i, b_.m128i), m), m); #else SIMDE_VECTORIZE for (size_t i = 0; i < (sizeof(r_.values) / sizeof(r_.values[0])); i++) { int64_t tmp = HEDLEY_STATIC_CAST(int64_t, a_.values[i]) - HEDLEY_STATIC_CAST(int64_t, b_.values[i]); r_.values[i] = HEDLEY_STATIC_CAST(int32_t, tmp < 0 ? -tmp : tmp); } #endif return simde_int32x4_from_private(r_); #endif } /* ======================================================= */ simde_int32x4_t simde_vaddq_s32(simde_int32x4_t a, simde_int32x4_t b) { #if defined(SIMDE_ARM_NEON_A32V7_NATIVE) return vaddq_s32(a, b); #elif defined(SIMDE_POWER_ALTIVEC_P6_NATIVE) return vec_add(a, b); #else simde_int32x4_private r_, a_ = simde_int32x4_to_private(a), b_ = simde_int32x4_to_private(b); #if defined(SIMDE_X86_SSE2_NATIVE) r_.m128i = _mm_add_epi32(a_.m128i, b_.m128i); #elif defined(SIMDE_VECTOR_SUBSCRIPT_OPS) r_.values = a_.values + b_.values; #else SIMDE_VECTORIZE for (size_t i = 0; i < (sizeof(r_.values) / sizeof(r_.values[0])); i++) { r_.values[i] = a_.values[i] + b_.values[i]; } #endif return simde_int32x4_from_private(r_); #endif }