内積命令

# はじめに
 - この記事は[ひとりNEONアドベントカレンダー2020](https://qiita.com/advent-calendar/2020/neon-only) 20日目の記事です
 - [昨日は`mov`命令を紹介](https://qiita.com/tomoaki_teshima/items/d97ea7aaefd3f7e8e2f7)した
 - 本日は内積`dot`命令を紹介する

![dot.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12162/202b77ac-7f74-21d7-f8bb-985893c062b9.png)

```bash
$ grep ^vdot /usr/lib/gcc/aarch64-linux-gnu/7.5.0/include/arm_neon.h
vdot_u32 (uint32x2_t __r, uint8x8_t __a, uint8x8_t __b)
vdotq_u32 (uint32x4_t __r, uint8x16_t __a, uint8x16_t __b)
vdot_s32 (int32x2_t __r, int8x8_t __a, int8x8_t __b)
vdotq_s32 (int32x4_t __r, int8x16_t __a, int8x16_t __b)
vdot_lane_u32 (uint32x2_t __r, uint8x8_t __a, uint8x8_t __b, const int __index)
vdot_laneq_u32 (uint32x2_t __r, uint8x8_t __a, uint8x16_t __b,
vdotq_lane_u32 (uint32x4_t __r, uint8x16_t __a, uint8x8_t __b,
vdotq_laneq_u32 (uint32x4_t __r, uint8x16_t __a, uint8x16_t __b,
vdot_lane_s32 (int32x2_t __r, int8x8_t __a, int8x8_t __b, const int __index)
vdot_laneq_s32 (int32x2_t __r, int8x8_t __a, int8x16_t __b, const int __index)
vdotq_lane_s32 (int32x4_t __r, int8x16_t __a, int8x8_t __b, const int __index)
vdotq_laneq_s32 (int32x4_t __r, int8x16_t __a, int8x16_t __b, const int __index)
```

# `dot`命令
 - 引数は`int32x4_t`型を1つ、`int8x16_t`型を2つ取る。もしくは符号なしの全く同じ型を引数に取る
 - 第2引数と第3引数の各要素ごとに積を求め、**4つずつまとめて総和を取る**
 - そして第1引数に加算する

# `dot_lane`命令
 - 4要素の積をとって1つの和に集約して、第1引数に加算する演算は`dot`命令と同じ
 - 対応する要素を使うのでなく、第3引数のうち、第4引数で指定したレーンを使用する
    - なお、第3引数は`int8x16_t`型だが、指定できるレーンは`[0:3]`の範囲。つまり、第3引数は要素が16個あるが、4要素ごとの集まりとみなしている
 - [`dup_lane`命令](https://qiita.com/tomoaki_teshima/items/6ef058adbdea8969480a#dup_lane)と同じく`q`が付く場所によって、4パターン存在する

| 命令 | 第1、第2引数及び戻り値のbit幅 |第3引数のbit幅|
|:-:|:-:|:-:|
|`dot_lane`  | 64bit | 64bit |
|`dotq_lane` |128bit | 64bit |
|`dot_laneq` | 64bit |128bit |
|`dotq_laneq`|128bit |128bit |

# OpenCVでの対応状況
 - ついでに、[OpenCVでの状況を覗いて見た](https://github.com/opencv/opencv/blob/f2fe6f40c262747b8d37ca167a37db371f4d3918/modules/core/include/opencv2/core/hal/intrin_neon.hpp)(2020/12/16時点、masterブランチ(OpenCV 4.1.2以降相当))

```intrin_neon.hpp
inline v_uint32x4 v_dotprod_expand(const v_uint8x16& a, const v_uint8x16& b)
{
#if CV_NEON_DOT
    return v_uint32x4(vdotq_u32(vdupq_n_u32(0), a.val, b.val));
#else
    const uint8x16_t zero   = vreinterpretq_u8_u32(vdupq_n_u32(0));
    const uint8x16_t mask   = vreinterpretq_u8_u32(vdupq_n_u32(0x00FF00FF));
    const uint16x8_t zero32 = vreinterpretq_u16_u32(vdupq_n_u32(0));
    const uint16x8_t mask32 = vreinterpretq_u16_u32(vdupq_n_u32(0x0000FFFF));

    uint16x8_t even = vmulq_u16(vreinterpretq_u16_u8(vbslq_u8(mask, a.val, zero)),
                                vreinterpretq_u16_u8(vbslq_u8(mask, b.val, zero)));
    uint16x8_t odd  = vmulq_u16(vshrq_n_u16(vreinterpretq_u16_u8(a.val), 8),
                                vshrq_n_u16(vreinterpretq_u16_u8(b.val), 8));

    uint32x4_t s0 = vaddq_u32(vreinterpretq_u32_u16(vbslq_u16(mask32, even, zero32)),
                              vreinterpretq_u32_u16(vbslq_u16(mask32, odd,  zero32)));
    uint32x4_t s1 = vaddq_u32(vshrq_n_u32(vreinterpretq_u32_u16(even), 16),
                              vshrq_n_u32(vreinterpretq_u32_u16(odd),  16));
    return v_uint32x4(vaddq_u32(s0, s1));
#endif
}
```
 - `v_dotprod_expand`命令の内部で`vdotq_u32`が使われているのが確認できる
 - しかし、肝心の`CV_NEON_DOT`が`0`で決め打ちなので、ちゃんとまだサポートされてないっぽい

```intrin_neon.hpp
// TODO
#define CV_NEON_DOT 0
```


# 追記
 - 初日にArm v8.2命令には深入りしない、と書いたものの、この命令は`dotprod`拡張命令であり、Arm v8.2で導入された拡張命令の一部である
 - 残念ながら手元には`dotprod`命令に対応したSoCを載せたSBCは無いので、解説はこれぐらいで

# おわりに
 - 今日は内積を計算する`dotprod`命令を紹介した[^a]
 - 明日も手島の執筆予定で、何を書こう。。。。

[^a]: 内積命令と言っているけれど、8bit4つをとって32bitに足し合わせる挙動はINT8のDNN推論演算そのものである。なので、おそらくDeep Learningのブームを背景に追加された命令だと推測する（個人の感想です）