はじめに
.NETのVector classを利用すると、C#でCPUのSSEやAVXなどを活用した高速化が実現できる。このとき、条件分岐を含む処理は、Vector.ConditionalSelect
を用いて記述できるが、Microsoft Docsの説明では記述例が不足していると思うので、実例を示す。
Vector化する前の処理
例として、HLSで用意された画像のピクセル情報から、RGBのピクセル情報を生成する処理を取り上げる。
HLS; Hue(色相), Lightness(明度), Saturation(彩度)
RGB; R(Red), G(Green), B(Blue)
処理では、色相の範囲により、RGBの各値の計算方法の相違により、条件分岐を行う。
- 色相(hue)はPCCS(日本色研配色体系)の定義により、0~24の値をとる。
- cMax, CMin, cDeltaは、欄外で計算済。
- Vector化したときの並列度向上のため、
double
ではなく、float
を選択。
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呼び出し毎に生成しない。
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。
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に相当する値。
- 変数を使い回しせず増やしているのは、デバッグの便を考えたもの。
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
// 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
が終了した時点の変数表を下図に示す。
Windowの色名Orange(#FFFFA5
)のピクセルの明度を増やした結果をRGBに戻す処理で、Vectorは4要素からなり、色相Vector(hvec5)の各要素の値は6.6ほど。
LessThanOrEqual
の処理では、4以下を表すVector(isHlte4)のみが全ての要素が偽。GreaterThan
の処理では、4より大を表すVector(isHgt4)のみが全ての要素が真。この2つを合成した結果、4より大きく8以下を表すVector(isHlte8gt4)の要素が真になる。
この結果、RGBのG(Green)の値についていえば、計算結果のVector(gv6)には、事前計算結果のVector(gvec84)の各値が設定されている。
高速化の度合い
処理例を見てわかるとおり、全ての条件について計算するなど演算量は少なくなく、並列化の効果を減殺しないか気になる。実際に、計測してみると、1024×640(約65万)ピクセルの画像で、次の通り。
- 直列処理 200~300ms
- Vector処理(8並列, AVX2あり) 70~80ms
それなりの効果は計測できるが、実際のアプリでは、SoftwareBitmap
の処理などが重く、体感できる量は小さくなる。手間に見合うかどうかは、アプリの性質次第。
考慮点
- 計算式を変形して、method呼び出し毎に変化しない部分を事前に計算することで、一層の高速化が期待できる場合がある。ただし、除算を先に行い有効桁数を下げることや、オーバーフローの発生に留意が必要になる。
- 処理例では、デバッグの便を考慮し、変数の使い回しをせず、methodの呼び出し毎に多数の変数を生成しているが、高速化のためには改善の余地がある。