tl;dr
cv::parallel_for_は、実行環境ごとにバックエンドが選ばれるので実行環境を考えず使える。しかし、
OpenCVオフィシャルサイトのHow to use the OpenCV parallel_for_ to parallelize your code](https://docs.opencv.org/4.1.0/d7/dff/tutorial_how_to_use_OpenCV_parallel_for_.html)
のサンプルコードがわかりにくい。もしfor文の並列処理をOpenMPで書くなら、
#pragma omp parallel for
for(int icx= 0,idx<100000,idx++){
//何かの処理
});
と同様に書きたいだけなのに・・・Rangeって何だ?どうやって使う?ってことで解説し、扱いやすいラッパー関数も作ってみた。ちなみに、もう2019年、functorは使わずラムダ式でいいでしょ。OpenCV4.0からc++11必須になったことだし。
サンプルコード
以下のような関数を用意して使えば、ラムダ式を使うことになるがOpenMPと同様でわかりやすい。
#include <thread> //std::thread::hardware_concurrency()をコールするため
template<class FUNC>
void paral_for_idx(int st, int ed, FUNC func)
{
int num_cpu = std::thread::hardware_concurrency();//論理cpu数取得
int nstripes = (ed - st + num_cpu -1) / num_cpu;
cv::parallel_for_(cv::Range(st, ed),
[&func](const cv::Range& range) {
for(int idx = range.start; idx < range.end; idx++) {
func(idx);
}
},nstripes);
}
//使い方1
paral_for_idx(0,100000,[&](int idx){
//何かの処理
});
//使い方2 画像処理
Mat3b mat;
paral_for_idx(0,mat.rows,[&](int y){
for(int x=0;x<mat.cols;x++){
//何かの処理 ←mat(y,x)で画素値を参照できる
}
});
解説
1つめのポイントは、paral_for_idxのラムダ式に渡される引数を一つにしたことで、記述しやすい。
2つめのポイントは、paral_for_idxの中でnstripesを計算し、引数として渡していること。cv::parallel_for_のラムダ式は引数にレンジを渡すので、このようにcpu数で割って切り上げた値をnstripesとして設定すると、cv::parallel_for_が連続してまとまった単位で処理してくれる。
cv::parallel_for_は、nstripesがデフォルト値のままだとどうも1単位ずつになるようで、例えば、使い方2の場合行ごとに別スレッドになってしまい、キャッシュ効率が悪い。上下画素も参照するフィルタ処理でキャッシュ効率を上げるなら、当然、近い行は同じCPUが同一スレッドで処理したほうがよい。
ちなみに、今回のnstripesの計算は「何かの処理」が同じ処理時間であることが前提なので、箇所によって処理時間が変動する処理ならnstripesを小さめにとって、スレッドの粒度を小さくし、解放されたCPUから次の処理を随時こなすようにしたほうがよい。ただ、そこまで制御したいならOpenMPでDyanamic指定を加えたほうがよいのかも。結局、OpenMPでも他の並列化フレームワークでも、最速を目指すなら環境とデータ特性に合わせて最適な並列化コードにチューニングする必要がある。環境を考えず、お手軽に使いたいなら、cv::parallel_for_を使ったほうが楽で、そのときparal_for_idxようなラッパー関数を用意しておくと、キャッシュ効率がよく、関数としても使い勝手がよい。