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` 表示, 例如:

  1. int8x8_t v[q]add_s8(int8x8_t a,int8x8_t b), 加法
  2. int8x8_t v[q]movn_s16(int16x8_t a), int16 转换为 int8
  3. int8x8_t v[q]shl_s8(int8x8_t a,int8x8_t b), 左移
  4. 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, 例如:

  1. int8x8_t v[r]shl_s8(int8x8_t a,int8x8_t b), 相当于 (a+(1<<(n-1)))>>b
  2. int8x8_t v[r]hadd_s8(int8x8_t a,int8x8_t b), 因为 hadd 相当于 (a+b)>>1, 所以也有 rounding 的问题

除了整数右移, vrndvcvt 指令支持指定不同的 rounding 方法, NEON 支持的 rounding 方法主要有:

  1. 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)

  2. 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)

  3. round toward minus infinity

    相当于 floor, 在指令时使用 `m` 前缀表示, 例如:

    float32x2_t vrnd[m]_f32(float32x2_t a)

    int32x2_t vcvt[m]_s32_f32(float32x2_t a)

  4. round toward plus infinity

    相当于 ceil, 在指令时使用 `p` 前缀表示, 例如:

    float32x2_t vrnd[p]_f32(float32x2_t a)

    int32x2_t vcvt[p]_s32_f32(float32x2_t a)

  5. 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 时:

  1. b 代表 int8_t
  2. h 代表 HI, 即 int16_t
  3. s 代表 SI, 即 int32_t
  4. d 代表 DI, 即 int64_t

当操作 float 时:

  1. s 代表 single precision, 即 float
  2. 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 的基本操作时主要有以下几个问题:

  1. neon 支持 64/128 bit vector, msa 只支持 128 bit, 导致 msa 实现类似于 vadd_s8 的指令时会浪费一半的计算
  2. msa 不支持 neon 的 widen/narrow 类型的指令, 例如 int16x8_t vaddl_s8(int8x8_t a, int8x8_t b), 需要额外的代码把 msa 的输入变成 int16x8_t, 而 msa 又不支持用于数据类型转换的 vmov 指令, 导致转换很低效.
  3. msa 不支持 neon 的 signed/unsigned 类型指令, 例如 int8x8_t vuqadd_s8(int8x8_t a,uint8x8_t b), 因为 msa 的输入输出类型基本上总是相同的, 需要手动做额外的数据转换
  4. msa 不支持 neon 的 high 类型的指令, 例如 int16x8_t vaddl_high_s8(int8x16_t a, int8x16_t b), 需要手动从 int8x16_t 构造 int16x8_t
  5. msa 不支持 cross vector 类型的指令, 例如 int8_t vaddv_s8(int8x8_t a)
  6. 当涉及到 neon 的 pairwise 操作时(例如 int16x4_t vpadal_s8(int16x4_t a,int8x8_t b)) 且 neon 是 64-bit vector 时, msa 会因为 vector 后一半填充的 0 导致 pairwise 结果错误.
  7. msa 不支持 lane 指令和 scalar 指令
  8. 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 定义了两种相关的类型:

  1. simde_int32x4_t
  2. 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
}

1.5. RISC-V

Author: [email protected]
Date: 2023-04-27 Thu 18:01
Last updated: 2023-06-29 Thu 18:30

知识共享许可协议