可変ベクトル長に合わせたSIMD実装はより素敵です

この記事は[OpenCV Advent Calendar 2024](https://qiita.com/advent-calendar/2024/opencv)の9日目の記事です。
他の記事は目次にまとめられています。

# TL;DR
- 今どきのSIMDは、ベクトル長が可変であることも少なくない
- これからのSIMD実装は、可変ベクトル長対応し、「ものすごいCPU」でも「そこそこCPU」 でも大丈夫なように作りましょう！

# はじめに
@tomoaki_teshima先生の2022年投稿がこちらの「OpenCV の dispatch機能について」でございます。

https://qiita.com/tomoaki_teshima/items/19333187613a700660aa

そういえば、WAYLAND対応の時にRAW RGBに変換するべくSIMD実装検討もしたなあ、ということで、今どきのSIMDの書き方をまとめておきたい。もし、SIMD実装しなきゃいけなくなったときに思い出していただけますと幸いです。

# SIMD技術の今昔

（ものすごくざっくりです）MMXとかSSEとかのSIMD創世記は、ベクトル長が非常に重要でした。64bit, 128bit, 256bit .... 大きければ大きいほどまとめて処理できる＝性能アップ。ただ、常に使うわけでもないし、新しい技術を導入すると後方互換性がなくなる、となかなか悩ましい問題でございました。

| CPU | Tech | Vec Len |
|:---:|:-----:|:-------:|
| x86 | MMX | 64bit |
| x86 | SSE | 128bit |
| x86 | AVX | 256bit |
| x86 | AVX-512 | 512bit |
| ARM | NEON | 64bit / 128bit |

しかも最近は、消費電力削減のために、複数種類のCPUを合わせたヘテロジニアウスな構成も出てきている。
その中で、人類は気が付いたのです。「更なる自由を！」

ARMのScalable Vector Extension(SVE)やRISC-VのVector Extensionは、ベクトル長が可変となっている。つまり、一般普及用の短いベクトル長のCPUでも、High performance computing用の長いベクトル長のCPUでも、同じ命令が処置できる！！

つまり
（１）Intel CPUでいえば、AVX-512対応しているP-coreでも、AVX2どまりのE-coreでも。
（２）ARM CPUでいえば、BIG(Cortex-A725)でもlittle(Cortex-A520)でも。
どちらでも、同じ命令が実行できるということでございまする！！これは素敵でございますねえ！！！

| CPU | Tech | Vec Len |
|:---:|:-----:|:-------:|
| x86 | AVX10.2 | 128, 256, 512 |
| ARM | SVE/SVE2 | 128～2048|
| RISC-V | Vector Extension | 32～2^16 |

# 今どきではないSIMD実装の書き方（SIMDベタ書き）

まずは、ベタ書きパターンを見ていく。

https://github.com/opencv/opencv/blob/4.10.0/modules/imgproc/src/median_blur.simd.hpp

- CV_SIMD128やCV_SIMD256を使ってコードブロックを制御
- 変数の型（v_uint16x16とc_uint16x8）が混在し、超絶、読みにくい
- 新しいSIMD技術がでると作り直しが必要

```cpp
                    /* Update corresponding histogram segment */
#if CV_SIMD256
                    v_uint16x16 v_fine;
#elif CV_SIMD128
                    v_uint16x8 v_finel;
                    v_uint16x8 v_fineh;
#endif
                    if ( luc[k] <= j-r )
                    {
#if CV_SIMD256
                        v_fine = v256_setzero_u16();
#elif CV_SIMD128
                        v_finel = v_setzero_u16();
                        v_fineh = v_setzero_u16();
#else
                        memset(&H.fine[k], 0, 16 * sizeof(HT));
#endif
                        px = h_fine + 16 * (n*(16 * c + k) + j - r);
                        for (luc[k] = HT(j - r); luc[k] < MIN(j + r + 1, n); ++luc[k], px += 16)
                        {
#if CV_SIMD256
                            v_fine = v_add(v_fine, v256_load(px));
#elif CV_SIMD128
                            v_finel = v_add(v_finel, v_load(px));
                            v_fineh = v_add(v_fineh, v_load(px + 8));
#else
                            for (int ind = 0; ind < 16; ++ind)
                                H.fine[k][ind] += px[ind];
#endif
                        }
```

# 今どきのSIMD実装

それでは、レーンを使った「今どきのSIMD実装」も確認しておく。

https://github.com/opencv/opencv/blob/4.10.0/modules/imgproc/src/smooth.simd.hpp

- VTraits<v_uint16>::vlanes();で、レーン数（まとめて処理できるデータ件数）を取得
- 変数の型（＝ベクトル長）を気にせずに実装でき、非常に読みやすい
- よりベクトル長が大きくなっても、コード修正なしで対応できる（厳密にはCMakefile修正は必要）

```cpp
        src += cn; dst += cn;
        int i = cn, lencn = (len - 1)*cn;
#if (CV_SIMD || CV_SIMD_SCALABLE)
        const uint16_t* _m = (const uint16_t*)m;
        const int VECSZ = VTraits<v_uint16>::vlanes();
        v_uint16 v_mul0 = vx_setall_u16(_m[0]);
        v_uint16 v_mul1 = vx_setall_u16(_m[1]);
        for (; i <= lencn - VECSZ; i += VECSZ, src += VECSZ, dst += VECSZ)
            v_store((uint16_t*)dst, v_add(v_mul(v_add(  vx_load_expand(src - cn), vx_load_expand(src + cn)),  v_mul0), v_mul(vx_load_expand(src), v_mul1)));
#endif
        for (; i < lencn; i++, src++, dst++)
            *((uint16_t*)dst) = saturate_cast<uint16_t>(((uint16_t*)m)[1] * (uint32_t)(src[0]) + ((uint16_t*)m)[0] * ((uint32_t)(src[-cn]) + (uint32_t)(src[cn])));

```

# まとめ
SIMD技術はこれからもベクトル長が大きくなる方向で進化していくと考えられる。

そうした場合に、それぞれのベクトル長ごとに実装するなんて、面倒くさい！

これからは、可変ベクトル長さのSIMD技術を使いこなしましょう！ということでまとめさせていただきます。

ご精読、ありがとうございました！