NEONを実際に書いてみる

# はじめに
 - この記事は[ひとりNEONアドベントカレンダー2020](https://qiita.com/advent-calendar/2020/neon-only) 2日目の記事です
 - [昨日](https://qiita.com/tomoaki_teshima/items/6e913d12f26c87e9c165)の概要に続き、今日は実際にコードを書いてみます。

# 実装例
```cpp:main.cpp
#include <arm_neon.h>
#include <iostream>

int main(int argc, char** argv)
{
    unsigned char src0[] = {  0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15};
    unsigned char src1[] = {100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115};
    unsigned char dst [] = {  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0};
    uint8x16_t s0 = vld1q_u8(src0);
    uint8x16_t s1 = vld1q_u8(src1);
    uint8x16_t d  = vaddq_u8(s0, s1); // ここで足し算
    vst1q_u8(dst, d);
    for( auto i = 0;i < 16;i++)
    {
        std::cout << i << '\t' << (int)src0[i] << '\t' << (int)src1[i] << '\t' << (int)dst[i] << std::endl;
    }
    return 0;
}
```

 - 単一のファイルなので横着してコマンドベタ書きします

```:build(ARmv7)
g++ -mfpu=neon main.cpp
```

 - Arm v7 では、コンパイラに明示的にNEONを使うように指定する必要があります(`-mfpu=neon`)
 - Arm v8 では、基本命令セットに入っているので、指定する必要はありません

```:build(ARmv8)
g++ main.cpp
```

- 実行結果

```
0       0       100     100
1       1       101     102
2       2       102     104
3       3       103     106
4       4       104     108
5       5       105     110
6       6       106     112
7       7       107     114
8       8       108     116
9       9       109     118
10      10      110     120
11      11      111     122
12      12      112     124
13      13      113     126
14      14      114     128
15      15      115     130
```

 - 2つのベクトルに入っている要素が足されていることが分かります。
 - 実際に加算処理は`vaddq_u8`の1回しか行われていないのですが、16個のデータが一度に処理されています

# 解説
## include文
```cpp
#include <arm_neon.h>
```
 - 基本的な命令はこいつを`include`するだけですべて使えるようになります
 - GCCの場合、だいたい `/usr/lib/gcc/aarch64-linux-gnu/<GCCのバージョン>/include/arm_neon.h`
 - Raspberry Pi 4の場合は `/usr/lib/gcc/aarch64-linux-gnu/7.5.0/include/arm_neon.h`

## データ型について
 - コード中、`uint8x16_t`という型を使っています
   - `<データ型>x<レーン数>_t`というフォーマットで指定します。

```cpp
    uint8x16_t s0 = vld1q_u8(src0);
```
 - 基本的な型は以下の9つです

| 型名 | 各要素の型 | レーン数 |
|:--|:--|:-:|
|`uint8x16_t`|`uint8_t` |16 |
|`uint16x8_t`|`uint16_t` |8 |
|`uint32x4_t`|`uint32_t` |4 |
|`uint64x2_t`|`uint64_t` |2 |
|`int8x16_t`|`int8_t` |16 |
|`int16x8_t`|`int16_t` |8 |
|`int32x4_t`|`int32_t` |4 |
|`int64x2_t`|`int64_t` |2 |
|`float16x8_t`|`float16_t` |8 |
|`float32x4_t`|`float32_t` |4 |
|`float64x2_t`|`float64_t` |2 |



 - 例えば`uint16`は16bit幅の符号なし整数なので、一般的に`unsigned short`と呼ばれる型です。
    - `uint16x8_t`は、`unsigned short`8個分がまとまって処理できます
 - 上の9つの型は情報量として128bit分保持できます。
 - Arm v8 では128bitレジスタを使い、Arm v7 では64bitレジスタ2つをくっつけて128bitレジスタとして振る舞います
 - また、全型は、その半分の64bit幅のデータ型が存在します

| 型名 | 各要素の型 | レーン数 |
|:--|:--|:-:|
|`uint8x8_t`|`uint8_t` |8 |
|`uint16x4_t`|`uint16_t` |4 |
|`uint32x2_t`|`uint32_t` |2 |
|`uint64x1_t`|`uint64_t` |1 |
|`int8x8_t`|`int8_t` |8 |
|`int16x4_t`|`int16_t` |4 |
|`int32x2_t`|`int32_t` |2 |
|`int64x1_t`|`int64_t` |1 |
|`float16x4_t`|`float16_t` |4 |
|`float32x2_t`|`float32_t` |2 |
|`float64x1_t`|`float64_t` |1 |

 - 注意が必要なのは、Arm v7では、`double`の演算はサポートされていません。そのため、Arm v7(もしくはRaspberry Pi 3 + 32bit Raspbianのような32bit OS on Arm v8)では、`float64x2_t`、`float64x1_t`は利用できません。
 - また、`float16x8_t`、`float16x4_t`は`NEON`の`FP16`拡張であり、Armv7では、別途別フラグを確認する必要があります。(詳しくは後日解説)
 - また、`poly8x16_t`、`poly16x8_t`、`poly64x2_t`という「多項式型」と言うのも存在します。
    - が、有効な利用方法及び原理を筆者が理解していないため、今回のアドベントカレンダーでは全面的に割愛します
    - Githubで検索しても、引っかかるのはGCCのテストコードが何千ファイルと同じものが引っかかるだけで、具体的に利用している人をGithub上で見たことはありません。
    - 多分`CRC`や`SHA1`などを計算するのに必要っぽいのだけれど、不明です。


## 組み込みSIMD命令について
 - サンプルコードでは `vaddq_u8`という命令を使いました。

```cpp
    uint8x16_t d  = vaddq_u8(s0, s1); // ここで足し算
```
 - 各命令は接頭辞の`v`で始まり、`v<命令><bit幅>_<データ型>`というフォーマットで表されます。
    - 末尾のデータ型は、引数によって変わり、以下の14種類があります
    - 符号なし整数: `u8`、`u16`、`u32`、`u64`
    - 符号あり整数: `s8`、`s16`、`s32`、`s64`
    - 浮動小数点型: `f16`、`f32`、`f64`
    - 多項式型: `p8`、`p16`、`p64`
 - 今回は`add`命令で、128bit分のデータを利用しました。この場合は末尾に`q`が付きます。
    - 64bit幅分の同様の命令は`vadd_u8`です
    - 基本的に64bit幅でも128bit幅でも、演算の内容は同じです
    - `vadd`に対して`vaddq`の様に64bit/128bit幅でサイズが違うだけの命令の対が多数存在します。
 - 以上の規則、みたいなものを頭の片隅に入れて本アドベントカレンダーを読んで頂ければより理解が深まるのではないかと思います。

## ロードとストアについて
 - `vld1q_u8`がメモリから読んでレジスタに格納するロード命令
 - `vst1q_u8`がレジスタからメモリに格納するストア命令

```cpp
    uint8x16_t s0 = vld1q_u8(src0);
    uint8x16_t s1 = vld1q_u8(src1);
    uint8x16_t d  = vaddq_u8(s0, s1); // ここで足し算
    vst1q_u8(dst, d);
```
 - `load`と`store`に関しては後日解説します
 - ベクトルの値から直接`print`するには、一度メモリに格納する必要があるため、そうしています。


# おわりに
 - サンプルコードを示してSIMD演算を行いました
 - 明日も手島の担当で、`add`命令の解説から順番に始めます。