zip命令

# はじめに
 - この記事は[ひとりNEONアドベントカレンダー2020](https://qiita.com/advent-calendar/2020/neon-only) 22日目の記事です
 - [昨日はfp16命令を紹介](https://qiita.com/tomoaki_teshima/items/86dcc0c233d59776de69)した
 - 今日は`zip`命令を紹介する
 - めずらしくこの命令はArm v7とArm v8でintrinsicが違う

![zip.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12162/4490c993-d087-8e94-f867-1f9d8316f5ab.png)

## Arm v7の場合
 - Raspbian OS 9.9 (Raspberry Pi 3)

![zip32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12162/bde8fb59-1052-007a-698a-4792892814bf.png)

```
$ grep ^vzip $MY_NEON_FILE | cut -f 1 -d _ | sort | uniq -c
      9 vzip
      9 vzipq
```
 - シグネチャ(一部修正)

```cpp
float32x4x2_t vzipq_f32 (float32x4_t __a, float32x4_t __b)
{
```

## Arm v8の場合
 - Ubuntu 18.04 (Jetson Nano)

![zip64.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12162/b7f0749d-badb-73df-2ba9-9bab7b38d872.png)

```
$ grep ^vzip /usr/lib/gcc/aarch64-linux-gnu/7.5.0/include/arm_neon.h  | cut -f 1 -d _ | sort | uniq -c
     10 vzip1
     13 vzip1q
     10 vzip2
     13 vzip2q
```

 - シグネチャ(一部修正)

```cpp
float32x4_t vzip1q_f32 (float32x4_t __a, float32x4_t __b)
{
```

# `zip`命令と`zip1`命令と`zip2`命令
 - 基本的には2つのベクトル変数を取り、各要素を互い違いにインターリーブする
 - `vzipq_f32`を例として、色付きで示した下図がわかりやすいと思う。

![zip.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12162/33ef1263-b004-35cb-8db0-ee2dd7a17cfe.png)

 - suffixが`f32`とは言え、レーン単位での並べ替えなので、`s32`や`u32`でも動きは変わりない。
 - 第1引数の要素0番、第2引数の要素0番、…と続き、第1引数の要素`n-1`番、第2引数の要素`n-1`番(`n`はレーン数)と続く。
 - 128bitベクトルを2つとって、要素を並べ替えるので戻り値も128bitベクトル2つ分になる
 - このときの戻り値の扱いで、Arm v7 と Arm v8 で挙動が違う

## Arm v7の場合
 - `zipq`命令の場合、128bit幅ベクトル2つを引数に取り、それぞれの要素をインターリーブする
 - 例えば、`vzipq_f32`命令の場合、戻り値は`float32x4x2_t`型となる
    - この`x2_t`が末尾に付いたやつは[load命令](https://qiita.com/tomoaki_teshima/items/4edde0fcd6436889ccd6)の回でも紹介した、ベクトルを複数束ねた構造体である
 - `int32x4_t`型が引数の場合は`int32x4x2_t`型が、`uint8x16_t`型が引数の場合は`uint8x16x2_t`型が戻り値となる
 - 128bitベクトル「2つ分」の値を返す必要があるため、構造体で返す形になる
 - アセンブラでも同様の挙動が見えて、以下のような命令で表される

```cpp
float32x4x2_t d = vzipq_f32(a, b);
```

```.s
vzip.32 q10, q11
```

 - アセンブラの`q10`と`q11`はそれぞれ128bit幅レジスタを表す
 - 2個のオペランドを取るが、出力のオペランドは指定されていない。となると、この命令は破壊的操作を伴う命令で、`q10`と`q11`に結果が書き出される

## Arm v8の場合
 - `vzip1q`命令も`vzip2q`命令も、どちらも128bit幅ベクトルを2つ引数に取るが、Arm v7と違い、戻り値は128bit幅ベクトル1つ分である
 - 前述の図で説明すると、インターリーブしたベクトルのうち、片方だけが出力される
    - 下図は`vipz1q`命令を表した場合

![zip1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12162/99958c3e-7166-1dde-cd51-2561446216a5.png)


 - これは、アセンブラでも確認できる


```cpp
float32x4x2_t d = vzipq_f32(a, b);
```

 - 本来、`zipq`命令はArm v8のgccのintrinsicには無いのだが、利便性を考えてか、gccの`arm_neon.h`では、マクロで一括宣言されている

```arm_neon.h
#define __DEFINTERLEAVE(op, rettype, intype, funcsuffix, Q)             \
  __extension__ extern __inline rettype                                 \
  __attribute__ ((__always_inline__, __gnu_inline__, __artificial__)) \
  v ## op ## Q ## _ ## funcsuffix (intype a, intype b)                  \
  {                                                                     \
    return (rettype) {v ## op ## 1 ## Q ## _ ## funcsuffix (a, b),      \
                      v ## op ## 2 ## Q ## _ ## funcsuffix (a, b)};     \
  }
```
 - これにより、`vzipq`命令は`vzip1q`命令と`vzip2q`命令を連続して呼ぶコードに変わる
 - アセンブラを見ると、`zip1`命令と`zip2`命令は、Arm v7のときと違い、3オペランドを取る


```.s
zip1    v2.4s, v2.4s, v3.4s
mov     v0.16b, v2.16b
 :
zip2    v2.4s, v2.4s, v3.4s
mov     v1.16b, v2.16b
```

 - これは、先頭から出力先、第1引数、第2引数で表される
 - ここでは`zip1`命令の出力結果は`v2`レジスタに書き出されるが、その後`v0`レジスタに退避されている
 - また、`zip2`命令の出力結果は`v2`レジスタに書き出された後、`v1`レジスタに退避されている


# OpenCVでの利用
 - 実際にOpenCVで使われている部分を見てみよう
 - いくつか利用されている箇所があるが、[RGBをYUV(YCrCb)に変換するコード](https://github.com/opencv/opencv/blob/fc1a15626226609babd128e043cf7c4e32f567ca/modules/imgproc/src/color_yuv.simd.hpp#L389)で使われている

```imgproc/src/color_yuv.simd.hpp
#if CV_SIMD
        const int vsize = v_uint8::nlanes;
        const int descaleShift = 1 << (shift-1);
        v_int16 bg2y;
        v_int16 r12y;
        v_int16 dummy;
        v_zip(vx_setall_s16((short)C0), vx_setall_s16((short)C1), bg2y, dummy);
        v_zip(vx_setall_s16((short)C2), vx_setall_s16( 1), r12y, dummy);
```
 - 最後2行に出てくる`v_zip`は、[OpenCVのUniversal Intrinsicという名前のラッパー](https://qiita.com/tomoaki_teshima/items/8195c25a3f0448ce300b)であり、Arm環境下では、[以下の通り、`zipq`命令か、`zip1q`と`zip2q`命令が呼ばれる](https://github.com/opencv/opencv/blob/fc1a15626226609babd128e043cf7c4e32f567ca/modules/core/include/opencv2/core/hal/intrin_neon.hpp)

```intrin_neon.hpp
#if defined(__aarch64__) || defined(_M_ARM64)
#define OPENCV_HAL_IMPL_NEON_UNPACKS(_Tpvec, suffix) \
inline void v_zip(const v_##_Tpvec& a0, const v_##_Tpvec& a1, v_##_Tpvec& b0, v_##_Tpvec& b1) \
{ \
    b0.val = vzip1q_##suffix(a0.val, a1.val); \
    b1.val = vzip2q_##suffix(a0.val, a1.val); \
} \
 :
#else
#define OPENCV_HAL_IMPL_NEON_UNPACKS(_Tpvec, suffix) \
inline void v_zip(const v_##_Tpvec& a0, const v_##_Tpvec& a1, v_##_Tpvec& b0, v_##_Tpvec& b1) \
{ \
    _Tpvec##x2_t p = vzipq_##suffix(a0.val, a1.val); \
    b0.val = p.val[0]; \
    b1.val = p.val[1]; \
} \
```
 - ここでは、`RGB`のうち、`G`と`B`要素、`R`要素と1をそれぞれインターリーブしている
 - これにより、`v_dotprod`命令で係数と掛け合わせた上で和を取る操作を行っている
 - `v_dotprod`命令の特性上、同じ画素の要素が横並びである必要があるため、`vld3`命令で要素ごとにメモリからロードした後、`zip`命令でインターリーブするというちょっと面倒くさいことをしている


# おわりに
 - 今日は`zip`命令を紹介した
 - 明日は転置の`transpose`命令を紹介する
 - 残り日数的に、なんとか完走できそう！？