C# .NETのVector classで条件分岐(if..then..else)

# はじめに
.NETのVector classを利用すると、C#でCPUのSSEやAVXなどを活用した高速化が実現できる。このとき、条件分岐を含む処理は、`Vector.ConditionalSelect`を用いて記述できるが、Microsoft Docsの説明では記述例が不足していると思うので、実例を示す。

# Vector化する前の処理
例として、HLSで用意された画像のピクセル情報から、RGBのピクセル情報を生成する処理を取り上げる。

:::note info
HLS; Hue(色相), Lightness(明度), Saturation(彩度)
RGB; R(Red), G(Green), B(Blue)
:::

処理では、色相の範囲により、RGBの各値の計算方法の相違により、条件分岐を行う。
- 色相(hue)はPCCS(日本色研配色体系)の定義により、0～24の値をとる。
- cMax, CMin, cDeltaは、欄外で計算済。
- Vector化したときの並列度向上のため、`double`ではなく、`float`を選択。

```C#:CPUによる1ピクセルごとの処理
float h = (float)hue;
float p;
float r;
float g;
float b;

if (h <= 4.0f)
{
    p = 4.0f;
    r = cMax;
    g = cMin;
    b = ((p - h) / 4.0f) * cDelta + cMin;
}
else if (h <= 8.0f)
{
    p = 4.0f;
    r = cMax;
    g = ((h - p) / 4.0f) * cDelta + cMin;
    b = cMin;
}
else if (h <= 12.0f)
{
    p = 12.0f;
    r = ((p - h) / 4.0f) * cDelta + cMin;
    g = cMax;
    b = cMin;
}
else if (h <= 16.0f)
{
    p = 12.0f;
    r = cMin;
    g = cMax;
    b = ((h - p) / 4.0f) * cDelta + cMin;
}
else if (h <= 20.0f)
{
    p = 20.0f;
    r = cMin;
    g = ((p - h) / 4.0f) * cDelta + cMin;
    b = cMax;
}
else
{
    p = 20.0f;
    r = ((h - p) / 4.0f) * cDelta + cMin;
    g = cMin;
    b = cMax;
}
```

# Vector化した後の処理

## 条件判定Vectorの作成

- 何度も使うVectorはclass変数で用意し、method呼び出し毎に生成しない。

```c#:class変数で準備
private Vector<float> _p4 = new Vector<float>(4f);
private Vector<float> _p8 = new Vector<float>(8f);
private Vector<float> _p12 = new Vector<float>(12f);
private Vector<float> _p16 = new Vector<float>(16f);
private Vector<float> _p20 = new Vector<float>(20f);
```

- hvec5は、色相(hue)を収めたVector。

```c#:条件判定Vector
Vector<Int32> isHlte4 = Vector.LessThanOrEqual(hvec5, _p4);
Vector<Int32> isHlte8 = Vector.LessThanOrEqual(hvec5, _p8);
Vector<Int32> isHlte12 = Vector.LessThanOrEqual(hvec5, _p12);
Vector<Int32> isHlte16 = Vector.LessThanOrEqual(hvec5, _p16);
Vector<Int32> isHlte20 = Vector.LessThanOrEqual(hvec5, _p20);

Vector<Int32> isHgt4 = Vector.GreaterThan(hvec5, _p4);
Vector<Int32> isHgt8 = Vector.GreaterThan(hvec5, _p8);
Vector<Int32> isHgt12 = Vector.GreaterThan(hvec5, _p12);
Vector<Int32> isHgt16 = Vector.GreaterThan(hvec5, _p16);
Vector<Int32> isHgt20 = Vector.GreaterThan(hvec5, _p20);

Vector<Int32> isHlte8gt4 = Vector.BitwiseAnd(isHlte8, isHgt4);
Vector<Int32> isHlte12gt8 = Vector.BitwiseAnd(isHlte12, isHgt8);
Vector<Int32> isHlte16gt12 = Vector.BitwiseAnd(isHlte16, isHgt12);
Vector<Int32> isHlte20gt16 = Vector.BitwiseAnd(isHlte20, isHgt16);
```

色相の上限と下限を`Vector.LessThanOrEqaul`と`Vector.GreaterThan`で判定し、`Vector.BitwiseAnd`で合成する。結果として得られるベクトルの要素は、偽の場合は0、真の場合は0以外が設定される(手許のVS2022で実行したところ`-1`)。

## 条件ごとの事前計算

Vectorの各要素が、直列処理の`if`文、`else`文のどの条件に合致するかは様々なので、全ての場合を事前に計算する。ここでは、色相(Hue)の範囲ごとに、減算(Subtract)、除算(Divide)、乗算(Multiply)、加算(Add)を行う。

- cdelta2は、直列処理のcDeltaに相当する値。
- 変数を使い回しせず増やしているのは、デバッグの便を考えたもの。

```C#:条件ごとの事前計算
Vector<float> bvec4 = Vector.Subtract(_p4, hvec5);
Vector<float> bvec42 = Vector.Divide(bvec4, _p4);
Vector<float> bvec43 = Vector.Multiply(bvec42, cdelta2);
Vector<float> bvec44 = Vector.Add(bvec43, cmin);

Vector<float> gvec8 = Vector.Subtract(hvec5, _p4);
Vector<float> gvec82 = Vector.Divide(gvec8, _p4);
Vector<float> gvec83 = Vector.Multiply(gvec82, cdelta2);
Vector<float> gvec84 = Vector.Add(gvec83, cmin);

Vector<float> rvec12 = Vector.Subtract(_p12, hvec5);
Vector<float> rvec122 = Vector.Divide(rvec12, _p4);
Vector<float> rvec123 = Vector.Multiply(rvec122, cdelta2);
Vector<float> rvec124 = Vector.Add(rvec123, cmin);

Vector<float> bvec16 = Vector.Subtract(hvec5, _p12);
Vector<float> bvec162 = Vector.Divide(bvec16, _p4);
Vector<float> bvec163 = Vector.Multiply(bvec162, cdelta2);
Vector<float> bvec164 = Vector.Add(bvec163, cmin);

Vector<float> gvec20 = Vector.Subtract(_p20, hvec5);
Vector<float> gvec202 = Vector.Divide(gvec20, _p4);
Vector<float> gvec203 = Vector.Multiply(gvec202, cdelta2);
Vector<float> gvec204 = Vector.Add(gvec203, cmin);

Vector<float> rvec24 = Vector.Subtract(hvec5, _p20);
Vector<float> rvec242 = Vector.Divide(rvec24, _p4);
Vector<float> rvec243 = Vector.Multiply(rvec242, cdelta2);
Vector<float> rvec244 = Vector.Add(rvec243, cmin);
```

それぞれのブロックの最後のVectorに、個々の条件ごとの計算結果が入る。

## ConditionalSelectを用いた条件ごとの事前計算結果の取捨選択

`Vector.ConditionalSelect`は、第1引数の判定ベクトルの個々の要素の値に従い、真なら第2引数、偽なら第3引数の値を選択し、結果のVectorを返す。`?:`三項演算子に近い動作をする。

ここでは、先に用意した条件判定Vectorを用い、事前計算結果のVectorを、Vectorの各要素ごとに選択して結果のVectorを生成する。

- _vUndefは、未定義値用のVector
- cminは、直列処理のcMinに相当するVector
- cmax2は、直列処理のcMaxに相当するVector
- bv,gv,rvは、BGR(RGBの各要素)に相当するVector

```C#:ConditionalSelectを用いた取捨選択
// if (h <= 4.0f)
Vector<float> bv = Vector.ConditionalSelect(isHlte4, bvec44, _vUndef);
Vector<float> gv = Vector.ConditionalSelect(isHlte4, cmin, _vUndef);
Vector<float> rv = Vector.ConditionalSelect(isHlte4, cmax2,_vUndef);

// else if (h <= 8.0f)
Vector<float> bv2 = Vector.ConditionalSelect(isHlte8gt4, cmin, bv);
Vector<float> gv2 = Vector.ConditionalSelect(isHlte8gt4, gvec84, gv);
Vector<float> rv2 = Vector.ConditionalSelect(isHlte8gt4, cmax2, rv);

// else if (h <= 12.0f)
Vector<float> bv3 = Vector.ConditionalSelect(isHlte12gt8, cmin, bv2);
Vector<float> gv3 = Vector.ConditionalSelect(isHlte12gt8, cmax2, gv2);
Vector<float> rv3 = Vector.ConditionalSelect(isHlte12gt8, rvec124, rv2);

// else if (h <= 16.0f)
Vector<float> bv4 = Vector.ConditionalSelect(isHlte16gt12, bvec164, bv3);
Vector<float> gv4 = Vector.ConditionalSelect(isHlte16gt12, cmax2, gv3);
Vector<float> rv4 = Vector.ConditionalSelect(isHlte16gt12, cmin, rv3);

// else if (h <= 20.0f)
Vector<float> bv5 = Vector.ConditionalSelect(isHlte20gt16, cmax2, bv4);
Vector<float> gv5 = Vector.ConditionalSelect(isHlte20gt16, gvec204, gv4);
Vector<float> rv5 = Vector.ConditionalSelect(isHlte20gt16, cmin, rv4);

// else
Vector<float> bv6 = Vector.ConditionalSelect(isHgt20, cmax2, bv5);
Vector<float> gv6 = Vector.ConditionalSelect(isHgt20, cmin, gv5);
Vector<float> rv6 = Vector.ConditionalSelect(isHgt20, rvec244, rv5);
```

各条件は重ねて適用する必要があるので、ブロックごとの結果を次のブロックの入力に使用している。ここでは、最終結果は、`bv6`,`gv6`,`rv6`に入る。

# デバッグ例

VisualStudio 2022をデバッグモードで動作させ、`ConditionalSelect`が終了した時点の変数表を下図に示す。

![ConditionalSelect.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2729778/8aa74aac-fd92-d555-d80b-5ba0d80d086a.png)

Windowの色名Orange(`#FFFFA5`)のピクセルの明度を増やした結果をRGBに戻す処理で、Vectorは4要素からなり、色相Vector(hvec5)の各要素の値は6.6ほど。
`LessThanOrEqual`の処理では、4以下を表すVector(isHlte4)のみが全ての要素が偽。`GreaterThan`の処理では、4より大を表すVector(isHgt4)のみが全ての要素が真。この2つを合成した結果、4より大きく8以下を表すVector(isHlte8gt4)の要素が真になる。

![ConditionalSelect2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2729778/5e5224c4-a493-4e75-7f5f-4e23da8df4f6.png)

この結果、RGBのG(Green)の値についていえば、計算結果のVector(gv6)には、事前計算結果のVector(gvec84)の各値が設定されている。

# 高速化の度合い

処理例を見てわかるとおり、全ての条件について計算するなど演算量は少なくなく、並列化の効果を減殺しないか気になる。実際に、計測してみると、1024×640(約65万)ピクセルの画像で、次の通り。

- 直列処理 200～300ms
- Vector処理(8並列, AVX2あり) 70～80ms

それなりの効果は計測できるが、実際のアプリでは、`SoftwareBitmap`の処理などが重く、体感できる量は小さくなる。手間に見合うかどうかは、アプリの性質次第。

# 考慮点

- 計算式を変形して、method呼び出し毎に変化しない部分を事前に計算することで、一層の高速化が期待できる場合がある。ただし、除算を先に行い有効桁数を下げることや、オーバーフローの発生に留意が必要になる。
- 処理例では、デバッグの便を考慮し、変数の使い回しをせず、methodの呼び出し毎に多数の変数を生成しているが、高速化のためには改善の余地がある。