2019年にOpenCVで適切にマルチスレッド化するにはどうすればいいですか? 質問する

2019年にOpenCVで適切にマルチスレッド化するにはどうすればいいですか? 質問する

背景:

OpenCV のマルチスレッドに関する記事や投稿をいくつか読みました。

  • 一方では、OpenCV の関数を内部的に並列化する TBB または OpenMP サポートを使用して OpenCV をビルドできます。
  • 一方、複数のスレッドを自分で作成し、関数を並列に呼び出すことで、アプリケーション レベルでマルチスレッドを実現できます。

しかし、どのマルチスレッド方法が正しい方法なのかについて一貫した答えを得ることができませんでした。

TBBに関しては、答え2012年から5つの賛成票を獲得:

WITH_TBB=ON の場合、OpenCV はいくつかの関数で複数のスレッドを使用しようとします。問題は、TBB でスレッド化されている関数が現時点ではごくわずか (おそらく 12 個) であることです。そのため、速度向上はほとんど見込めません。ここでの OpenCV の哲学は、OpenCV 関数ではなく、アプリケーションがマルチスレッド化されるべきだということです。[...]

アプリケーションレベルでのマルチスレッドに関しては、コメントモデレーターより回答.opencv.org:

opencv で独自のマルチスレッドを使用することは避けてください。多くの関数は明示的にスレッドセーフではありません。代わりに、TBB または openmp サポートを使用して opencv ライブラリを再構築してください。

しかし、別の答え3 件の賛成票を獲得したユーザーは次のように述べています:

ライブラリ自体はスレッドセーフなので、ライブラリへの複数の呼び出しを同時に行うことができますが、データは必ずしもスレッドセーフであるとは限りません。

問題の説明:

そのため、少なくともアプリケーション レベルで (マルチ) スレッドを使用するのは問題ないと考えました。しかし、プログラムを長時間実行すると、奇妙なパフォーマンスの問題が発生しました。

これらのパフォーマンスの問題を調査した後、最小限かつ完全で検証可能なサンプル コードを作成しました。

#include "opencv2\opencv.hpp"
#include <vector>
#include <chrono>
#include <thread>

using namespace cv;
using namespace std;
using namespace std::chrono;

void blurSlowdown(void*) {
    Mat m1(360, 640, CV_8UC3);
    Mat m2(360, 640, CV_8UC3);
    medianBlur(m1, m2, 3);
}

int main()
{
    for (;;) {
        high_resolution_clock::time_point start = high_resolution_clock::now();

        for (int k = 0; k < 100; k++) {
            thread t(blurSlowdown, nullptr);
            t.join(); //INTENTIONALLY PUT HERE READ PROBLEM DESCRIPTION
        }

        high_resolution_clock::time_point end = high_resolution_clock::now();
        cout << duration_cast<microseconds>(end - start).count() << endl;
    }
}

実際の動作:

プログラムが長時間実行されている場合、表示される時間は

cout << duration_cast<microseconds>(end - start).count() << endl;

どんどん大きくなっています。

プログラムを約 10 分間実行すると、印刷される時間範囲が 2 倍になりますが、これは通常の変動では説明できません。

予想される行動:

私が期待するプログラムの動作は、関数を直接呼び出す場合よりも長くなる可能性があるにもかかわらず、時間範囲がほぼ一定のままであることです。

ノート:

関数を直接呼び出す場合:

[...]
for (int k = 0; k < 100; k++) {
    blurSlowdown(nullptr);
}
[...]

印刷された時間範囲は一定のままです。

cv 関数を呼び出さない場合:

void blurSlowdown(void*) {
    Mat m1(360, 640, CV_8UC3);
    Mat m2(360, 640, CV_8UC3);
    //medianBlur(m1, m2, 3);
}

印刷された時間範囲も一定のままです。したがって、スレッドを OpenCV 関数と組み合わせて使用​​すると、何か問題が発生しているはずです。

  • 上記のコードでは実際のマルチスレッドは実現されず、関数を呼び出すスレッドは同時に 1 つしかアクティブにならないことはわかっていますblurSlowdown()
  • スレッドを作成してその後クリーンアップすることは無料ではなく、関数を直接呼び出すよりも遅くなることはわかっています。
  • それはないコードが全体的に遅いということについて。問題は、印刷された時間の範囲が時間が経つにつれてどんどん長くなる
  • この問題は、やなどmedianBlur()の他の関数でも発生するため、 関数とは関係ありません。erode()blur()
  • この問題は Mac の clang++ で再現されました。@Mark Setchell のコメントを参照してください。
  • リリースライブラリの代わりにデバッグライブラリを使用すると、問題はさらに深刻化する。

私のテスト環境:

  • Windows 10 64ビット
  • MSVC コンパイラ
  • 公式 OpenCV 3.4.2 バイナリ

私の質問:

  • OpenCV を使用してアプリケーション レベルで (マルチ) スレッドを使用しても大丈夫ですか?
  • もしそうなら、なぜ上記のプログラムによって時間範囲が印刷されるのか成長中時間とともに?
  • いいえの場合、なぜOpenCVがスレッドセーフとみなされるそして、どのように解釈するかを説明してくださいキリル・コルニャコフの声明その代わり
  • 2019 年の TBB / OpenMP は現在広くサポートされていますか?
  • はいの場合、アプリケーション レベルのマルチスレッド (許可されている場合) と TBB / OpenMP のどちらの方がパフォーマンスが優れていますか?

ベストアンサー1

まず、質問を明確にしていただきありがとうございます。

質問:OpenCV を使用してアプリケーション レベルで (マルチ) スレッドを使用しても大丈夫ですか?

答え:はい、ぼかしや色空間の変更など、マルチスレッドを活用できる関数を使用しない限り、OpenCV でアプリケーション レベルでマルチスレッドを使用することはまったく問題ありません。ここでは、画像を複数の部分に分割し、分割された部分全体にグローバル関数を適用してから、再結合して最終出力を得ることができます。

Hough、pca_analysis などの一部の関数は、分割された画像セクションに適用されてから再結合されたときに正しい結果が得られないため、そのような関数にアプリケーション レベルでマルチスレッドを適用すると正しい結果が得られない可能性があるため、実行しないでください。

πάντα ῥεῖ が述べたように、マルチスレッドの実装では for ループ自体でスレッドを結合するため、利点はありません。promise オブジェクトと future オブジェクトを使用することをお勧めします (方法の例が必要な場合は、コメントでお知らせください。スニペットを共有します)。

以下の回答には多くの調査が必要でした。質問をしていただきありがとうございます。マルチスレッドの知識に情報を追加するのに本当に役立ちます :)

質問:もしそうなら、上記のプログラムによって出力される時間範囲が時間の経過とともに長くなるのはなぜですか?

答え:いろいろ調べた結果、スレッドの作成と破棄には大量の CPU とメモリ リソースが必要であることがわかりました。スレッドを初期化すると (コード内のこの行でthread t(blurSlowdown, nullptr);)、この変数が指すメモリの場所に識別子が書き込まれ、この識別子によってスレッドを参照できるようになります。プログラムでは非常に高い頻度でスレッドの作成と破棄が行われていますが、これは次のようなことです。プログラムにはスレッド プールが割り当てられており、それを通じてプログラムはスレッドを実行したり破棄したりできます。簡単に説明します。以下の説明を見てみましょう。

  1. スレッドを作成すると、このスレッドを指す識別子が作成されます。
  2. スレッドを破棄すると、このメモリは解放されます

しかし

  1. 最初のスレッドが破棄されてからすぐに再度スレッドを作成すると、この新しいスレッドの識別子は新しい場所(前のスレッド以外の場所) スレッド プール内。

  2. スレッドを繰り返し作成して破棄した後、スレッドプールが枯渇しましたなどCPUはプログラムサイクルを少し遅くするように強制され、スレッドプールが再び解放されます。新しいスレッドのためのスペースを作るため。

Intel TBB と OpenMP はスレッド プールの管理に非常に優れているため、それらを使用している間はこの問題は発生しない可能性があります。

質問:2019 年の TBB は現在広くサポートされていますか?

答え:はい、OpenCV のビルド時に TBB サポートをオンにしながら、OpenCV プログラムで TBB の利点を活用することができます。

以下は medianBlur での TBB 実装のプログラムです。

#include "opencv2/imgproc/imgproc.hpp"
#include "opencv2/highgui/highgui.hpp"
#include <iostream>
#include <chrono>

using namespace cv;
using namespace std;
using namespace std::chrono;

class Parallel_process : public cv::ParallelLoopBody
{

private:
    cv::Mat img;
    cv::Mat& retVal;
    int size;
    int diff;

public:
    Parallel_process(cv::Mat inputImgage, cv::Mat& outImage,
                     int sizeVal, int diffVal)
        : img(inputImgage), retVal(outImage),
          size(sizeVal), diff(diffVal)
    {
    }

    virtual void operator()(const cv::Range& range) const
    {
        for(int i = range.start; i < range.end; i++)
        {
            /* divide image in 'diff' number
               of parts and process simultaneously */

            cv::Mat in(img, cv::Rect(0, (img.rows/diff)*i,
                                     img.cols, img.rows/diff));
            cv::Mat out(retVal, cv::Rect(0, (retVal.rows/diff)*i,
                                         retVal.cols, retVal.rows/diff));

            cv::medianBlur(in, out, size);
        }
    }
};

int main()
{
    VideoCapture cap(0);

    cv::Mat img, out;

    while(1)
    {
        cap.read(img);
        out = cv::Mat::zeros(img.size(), CV_8UC3);

        // create 8 threads and use TBB
        auto start1 = high_resolution_clock::now();
        cv::parallel_for_(cv::Range(0, 8), Parallel_process(img, out, 9, 8));
        //cv::medianBlur(img, out, 9); //Uncomment to compare time w/o TBB
        auto stop1 = high_resolution_clock::now();
        auto duration1 = duration_cast<microseconds>(stop1 - start1);

        auto time_taken1 = duration1.count()/1000;
        cout << "TBB Time: " <<  time_taken1 << "ms" << endl;

        cv::imshow("image", img);
        cv::imshow("blur", out);
        cv::waitKey(1);
    }

    return 0;
}

私のマシンでは、TBB の実装には約 10 ミリ秒かかり、TBB がない場合には約 40 ミリ秒かかります。

質問:はいの場合、アプリケーション レベルのマルチスレッド (許可されている場合) と TBB / OpenMP のどちらの方がパフォーマンスが優れていますか?

答え:TBB はスレッドをより良く制御でき、並列コードを書くための構造もより良く、内部的には pthread も管理するので、POSIX マルチスレッド (pthread/thread) よりも TBB/OpenMP を使用することをお勧めします。pthread を使用する場合は、コード内で同期や安全性などに注意する必要があります。ただし、これらのフレームワークを使用すると、非常に複雑になる可能性のあるスレッド処理の必要性が抽象化されます。

編集:画像のサイズと処理を分割するスレッド数の不一致に関するコメントを確認しました。潜在的回避策(テストはしていませんが、動作するはずです)として、次のように画像の解像度を互換性のある寸法に拡大縮小します。

画像の解像度が 485 x 647 の場合は、488 x 648 に拡大縮小してから渡して、Parallel_process出力を元のサイズの 458 x 647 に戻します。

TBBとOpenMPの比較についてはこの答え

おすすめ記事