![]() |
OpenCV 4.13.0
Open Source Computer Vision
|
前のチュートリアル: OpenCV の parallel_for_ を使ってコードを並列化する方法
| 互換性 | OpenCV >= 4.11 |
このチュートリアルの目的は、汎用イントリンシクス 機能を使ってC++コードをベクトル化し、実行を高速化するためのガイドを提供することである。まず SIMDイントリンシック とワイドな レジスタ の扱い方を簡単に見てから、ワイドレジスタを使った基本的な演算のチュートリアルへと進む。
この節では、機能をより理解しやすくするために、いくつかの概念を簡単に見ていく。
イントリンシックは、コンパイラによって個別に扱われる関数である。これらの関数は可能な限り効率的に動作するよう最適化されていることが多く、そのため通常の実装より高速に動作する。しかし、これらの関数はコンパイラに依存するため、移植性のあるアプリケーションを書くことが難しくなる。
SIMDは Single Instruction, Multiple Data の略である。SIMDイントリンシックは、プロセッサが計算をベクトル化することを可能にする。データは レジスタ と呼ばれるものに格納される。レジスタ は 128ビット、256ビット、あるいは 512ビット 幅をもつ。各 レジスタ は 同じデータ型 の 複数の値 を格納する。レジスタのサイズと各値のサイズによって、格納される値の総数が決まる。
CPUがどの 命令セット をサポートしているかに応じて、異なるレジスタを使用できる場合がある。詳しくは こちら を参照
VLAは Vector Length Agnostic の略である。レジスタ幅をコンパイル時に固定するのではなく、実行時にハードウェアによって決定する仕組みである。これにより、単一のバイナリが同一アーキテクチャ内の異なるCPU間でその性能をスケールできる(例: RVVやSVE)。
OpenCVのuniversal intrinsicsは、SIMDおよびVLAのベクトル化手法に対する抽象化を提供し、システム固有のコードを書く必要なくイントリンシックを利用できるようにする。サポートされるSIMD/VLA技術については 汎用イントリンシクス に詳しく記載されている。
ここで、利用可能な構造体と関数を紹介する:
Universal Intrinsicsのセットは、各レジスタを特定のSIMDレジスタに基づく構造体として実装している。すべての型は nlanes 列挙を含んでおり、これはその型が保持できる値の正確な個数を与える。これにより、実装中に値の個数をハードコードする必要がなくなる。
cv 名前空間の下にある。レジスタには 2種類 ある:
可変サイズレジスタ: これらの構造体は固定サイズをもたず、その正確なビット長は利用可能なSIMD機能に基づいてコンパイル時に推定される。その結果、nlanes 列挙の値はコンパイル時に決定される。
各構造体は次の規約に従う:
v_[type of value][size of each value in bits]
例えば、v_uint8 は8ビット符号なし整数を保持し、v_float32 は32ビット浮動小数点値を保持する。そして、C++で任意のオブジェクトを宣言するのと同じようにレジスタを宣言する
利用可能なSIMD命令セットに基づき、特定のレジスタは異なる個数の値を保持する。例えば: コンピュータが最大256ビットのレジスタをサポートしている場合、
v_uint8 a; // a is a register supporting uint8(char) data int n = a.nlanes; // n holds 32
利用可能なデータ型とサイズ:
| 型 | ビット単位のサイズ |
|---|---|
| uint | 8, 16, 32, 64 |
| int | 8, 16, 32, 64 |
| float | 32, 64 |
固定サイズレジスタ: これらの構造体は固定のビットサイズをもち、一定個数の値を保持する。システムがどのSIMD命令セットをサポートしているかを把握し、互換性のあるレジスタを選択する必要がある。正確なビット長が必要な場合にのみ使用すること。
各構造体は次の規約に従う:
v_[type of value][size of each value in bits]x[number of values]
次のものを格納したいとする
v_int32x8 reg1 // holds 8 32-bit signed integers.
v_float64x8 reg2 // reg2.nlanes = 8
レジスタの動作がわかったところで、これらのレジスタに値を詰めるために使う関数を見ていく。
ロード: ロード関数を使うと、レジスタに値を ロード できる。
float ptr[32] = {1, 2, 3 ..., 32}; // ptr is a pointer to a contiguous memory block of 32 floats // Variable Sized Registers // int x = v_float32().nlanes; // set x as the number of values the register can hold v_float32 reg1(ptr); // reg1 stores first x values according to the maximum register size available. v_float32 reg2(ptr + x); // reg stores the next x values // Constant Sized Registers // v_float32x4 reg1(ptr); // reg1 stores the first 4 floats (1, 2, 3, 4) v_float32x4 reg2(ptr + 4); // reg2 stores the next 4 floats (5, 6, 7, 8) // Or we can explicitly write down the values. v_float32x4(1, 2, 3, 4);
ロード関数 - load メソッドを使い、データのメモリアドレスを与えることができる:
float ptr[32] = {1, 2, 3, ..., 32};
v_float32 reg_var;
reg_var = vx_load(ptr); // loads values from ptr[0] upto ptr[reg_var.nlanes - 1]
v_float32x4 reg_128;
reg_128 = v_load(ptr); // loads values from ptr[0] upto ptr[3]
v_float32x8 reg_256;
reg_256 = v256_load(ptr); // loads values from ptr[0] upto ptr[7]
v_float32x16 reg_512;
reg_512 = v512_load(ptr); // loads values from ptr[0] upto ptr[15]
vx_load_aligned() 関数を使ってよい。float ptr[4]; v_store(ptr, reg); // store the first 128 bits(interpreted as 4x32-bit floats) of reg into ptr.
universal intrinsicsのセットは、要素ごとの二項演算と単項演算を提供する。
v_float32 a, b; // {a1, ..., an}, {b1, ..., bn} v_float32 c = v_add(a, b); // {a1 + b1, ..., an + bn} v_flaot32 d = v_mul(a, b); // {a1 * b1, ..., an * bn}
v_int32 as; // {a1, ..., an} v_int32 al = v_shl(as, 2); // {a1 << 2, ..., an << 2} v_int32 bl = v_shr(as, 2); // {a1 >> 2, ..., an >> 2} v_int32 a, b; v_int32 a_and_b = v_and(a, b); // {a1 & b1, ..., an & bn}
// let us consider the following code is run in a 128-bit register v_uint8 a; // a = {0, 1, 2, ..., 13, 14, 15} v_uint8 b; // b = {15, 14, 13, ..., 2, 1, 0} v_uint8 c = v_lt(a, b); // c = {255, 255, 255, ..., 0, 0, 0} /* let us look at the first 4 values in binary a = |00000000|00000001|00000010|00000011| b = |00001111|00001110|00001101|00001100| c = |11111111|11111111|11111111|11111111| If we store the values of c and print them as integers, we will get 255 for true values and 0 for false values. */ --- // In a computer supporting 256-bit registers v_int32 a; // a = {1, 2, 3, 4, 5, 6, 7, 8} v_int32 b; // b = {8, 7, 6, 5, 4, 3, 2, 1} v_int32 c = v_lt(a, b); // c = {-1, -1, -1, -1, 0, 0, 0, 0} /* The true values are 0xffffffff, which in signed 32-bit integer representation is equal to -1. */ v_int32 a; // {a1, ..., an} v_int32 b; // {b1, ..., bn} v_int32 mn = v_min(a, b); // {min(a1, b1), ..., min(an, bn)} v_int32 mx = v_max(a, b); // {max(a1, b1), ..., max(an, bn)} v_int32 a; // a = {a1, ..., a4} int mn = v_reduce_min(a); // mn = min(a1, ..., an) int sum = v_reduce_sum(a); // sum = a1 + ... + an v_uint8 a; // {a1, .., an} v_uint8 b; // {b1, ..., bn} v_int32x4 mask: // {0xff, 0, 0, 0xff, ..., 0xff, 0} v_uint8 Res = v_select(mask, a, b) // {a1, b2, b3, a4, ..., an-1, bn} /* "Res" will contain the value from "a" if mask is true (all bits set to 1), and value from "b" if mask is false (all bits set to 0) We can use comparison operators to generate mask and v_select to obtain results based on conditionals. It is common to set all values of b to 0. Thus, v_select will give values of "a" or 0 based on the mask. */ 以下の節では、シングルチャンネルに対する単純な畳み込み関数をベクトル化し、その結果をスカラー実装と比較する。
畳み込みについては前のチュートリアルでさらに学べる。前のチュートリアルと同じ素朴な実装を用い、それをベクトル化版と比較する。
チュートリアルの完全なコードは こちら にある。
まず1次元畳み込みを実装し、それをベクトル化する。2次元のベクトル化畳み込みは、正しい結果を生成するために行方向に1次元畳み込みを実行する。
ここで、1次元畳み込みのベクトル化版を見ていく。
step. We add these values to the already stored values in ans Mat オブジェクトに格納することもできる For example: kernel: {k1, k2, k3} src: ...|a1|a2|a3|a4|... iter1: for each idx i in (0, len), 'step' idx at a time kernel_wide: |k1|k1|k1|k1| window: |a0|a1|a2|a3| ans: ...| 0| 0| 0| 0|... sum = ans + window * kernel_wide = |a0 * k1|a1 * k1|a2 * k1|a3 * k1| iter2: kernel_wide: |k2|k2|k2|k2| window: |a1|a2|a3|a4| ans: ...|a0 * k1|a1 * k1|a2 * k1|a3 * k1|... sum = ans + window * kernel_wide = |a0 * k1 + a1 * k2|a1 * k1 + a2 * k2|a2 * k1 + a3 * k2|a3 * k1 + a4 * k2| iter3: kernel_wide: |k3|k3|k3|k3| window: |a2|a3|a4|a5| ans: ...|a0 * k1 + a1 * k2|a1 * k1 + a2 * k2|a2 * k1 + a3 * k2|a3 * k1 + a4 * k2|... sum = sum + window * kernel_wide = |a0*k1 + a1*k2 + a2*k3|a1*k1 + a2*k2 + a3*k3|a2*k1 + a3*k2 + a4*k3|a3*k1 + a4*k2 + a5*k3| カーネルが ksize 行をもつとする。特定の行の値を計算するには、前の ksize/2 行と次の ksize/2 行について、対応するカーネル行との1次元畳み込みを計算する。最終的な値は、個々の1次元畳み込みの単純な合計である
unsigned char 行列に変換する このチュートリアルでは、水平方向の勾配カーネルを用いた。両方の手法で同じ出力画像が得られる。
実行時間の改善はさまざまであり、CPUで利用可能なSIMD機能に依存する。