Как правильно Multithread в OpenCV в 2019 году?

Справочная информация:

Я прочитал несколько статей и сообщений, касающихся многопоточности в OpenCV:

  • С одной стороны, вы можете создавать OpenCV с поддержкой TBB или OpenMP, которые распараллеливают функции OpenCV внутри.
  • С другой стороны, вы можете создавать несколько потоков самостоятельно и вызывать параллельные функции для реализации многопоточности на уровне приложений.

Но я не мог получить последовательные ответы, какой метод многопоточности - правильный путь.

Что касается TBB, ответ answer 2012 года с 5 ответами:

С WITH_TBB = ON OpenCV пытается использовать несколько потоков для некоторых функций. Проблема в том, что на данный момент только TBF имеет много функций (может быть дюжина). Так что трудно увидеть какое-либо ускорение. Философия OpenCV заключается в том, что приложение должно быть многопоточным, а не функциями OpenCV. [...]

Что касается многопоточности на уровне приложения, комментарий от модератора на answers.opencv.org:

пожалуйста, избегайте использования своей собственной многопоточности с opencv. многие функции явно не поточнобезопасны. скорее перестройте библиотеки opencv с поддержкой TBB или openmp.

Но другой ответ с 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 минут время печати увеличилось вдвое, что невозможно объяснить обычными колебаниями.

Ожидаемое поведение:

Поведение программы, которое я ожидаю, состоит в том, что промежутки времени остаются почти постоянными, даже если они могут быть дольше, чем непосредственный вызов функции.

Примечания:

При непосредственном вызове функции:

[...]
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 должно быть что-то не так.

  • Я знаю, что приведенный выше код НЕ достигает фактической многопоточности, в то же время будет активен только один поток, вызывающий функцию blurSlowdown().
  • Я знаю, что создание потоков и их очистка после этого не будут бесплатными и будут медленнее, чем прямой вызов функции.
  • НЕ о том, что код вообще медленный. Проблема в том, что напечатанные промежутки времени становятся все длиннее и длиннее.
  • Эта проблема не связана с функцией medianBlur(), поскольку она возникает в других функциях, например, erode() или blur().
  • Проблема была воспроизведена под Mac под clang++, см. комментарий @Mark Setchell
  • Проблема усиливается при использовании библиотеки отладки вместо выпуска

Моя среда тестирования:

  • Windows 10 64bit
  • Компилятор MSVC
  • Официальные двоичные файлы OpenCV 3.4.2

Мои вопросы:

  • Можно ли использовать (мульти) многопоточность на уровне приложений с OpenCV?
  • Если да, почему промежутки времени, напечатанные моей программой, выше РОСТА с течением времени?
  • Если нет, почему OpenCV тогда считается потокобезопасным, и, пожалуйста, объясните, как вместо этого интерпретировать выражение Кирилла Корнякова instead
  • Широко ли поддерживается TBB/OpenMP в 2019 году?
  • Если да, что предлагает лучшую производительность, многопоточность на уровне приложений (если разрешено) или TBB/OpenMP?

Ответ 1

Прежде всего, спасибо за ясность вопроса.

В: Можно ли использовать (мульти) многопоточность на уровне приложений с OpenCV?

A: Да, вполне нормально использовать многопоточность на уровне приложений с OpenCV, если только вы не используете функции, которые могут использовать преимущества многопоточности, такие как размытие, изменение цветового пространства, здесь вы можете разделить изображение на несколько частей и применить глобальные функции по всей разделенной части, а затем рекомбинируйте их для получения окончательного результата.

В некоторых функциях, таких как Hough, pca_analysis, которые не могут дать правильные результаты, когда они применяются к разделенным разделам изображения и затем объединяются, применение многопоточности на уровне приложения к таким функциям может не дать правильных результатов и, следовательно, не должно выполняться.

Как уже упоминалось, ваша реализация многопоточности не даст вам преимущества, потому что вы присоединяетесь к потоку в самом цикле for. Я бы посоветовал вам использовать объекты обещания и будущие объекты (если вам нужен пример того, как это сделать, дайте мне знать в комментариях, я поделюсь фрагментом кода.

Ниже ответ потребовал много исследований, спасибо за вопрос, это действительно помогает мне добавить информацию к моим многопоточным знаниям :)

В: Если да, почему промежутки времени, напечатанные моей программой, растут с течением времени?

A: После долгих исследований я обнаружил, что создание и уничтожение потоков требует много ресурсов процессора и памяти. Когда мы инициализируем поток (в вашем коде этой строкой: thread t(blurSlowdown, nullptr);), идентификатор записывается в область памяти, на которую указывает эта переменная, и этот идентификатор позволяет нам обращаться к потоку. Теперь в вашей программе вы создаете и уничтожаете потоки с очень высокой скоростью, теперь это то, что происходит, для программы выделен пул потоков, с помощью которого наша программа может запускать и уничтожать потоки, я буду держать его коротким и давайте рассмотрим объяснение ниже:

  1. Когда вы создаете поток, это создает идентификатор, который указывает на этот поток.
  2. Когда вы уничтожаете поток, эта память освобождается

НО

  1. Когда вы снова создаете поток, и через некоторое время первый поток уничтожается, идентификатор этого нового потока указывает на новое местоположение(местоположение, отличное от предыдущего потока) в пуле потоков.

  2. После многократного создания и уничтожения потока, пул потоков исчерпан, и поэтому ЦП вынужден немного замедлять циклы нашей программы, так что пул потоков снова освобождается для освобождения места для новая тема.

Intel TBB и OpenMP очень хорошо справляются с управлением пулом потоков, поэтому эта проблема может не возникать при их использовании.

В: Широко ли поддерживается TBB в 2019 году?

О: Да, вы можете использовать преимущества TBB в своей программе OpenCV, а также включить поддержку TBB при создании OpenCV.

Вот программа для реализации TBB в medianBlur:

#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?

A: Я бы предложил использовать многопоточность TBB/OpenMP поверх POSIX (pthread/thread), потому что TBB предлагает вам лучший контроль над потоком + лучшую структуру для написания параллельного кода и внутренне он управляет pthreads. В случае, если вы используете pthreads, вам придется позаботиться о синхронизации, безопасности и т.д. В вашем коде. Но использование этих структур избавляет от необходимости обрабатывать потоки, которые могут быть очень сложными.

Изменить: Я проверил комментарии относительно несовместимости размеров изображения с номером нити, на которую вы хотите разделить обработку. Итак, вот потенциальный обходной путь (не тестировал, но должен работать), масштабируйте разрешение изображения до совместимых размеров, например:

Если разрешение изображения составляет 485 x 647, масштабируйте его до 488 x 648, затем передайте его в Parallel_process, а затем уменьшите масштаб до исходного размера 458 x 647.

Для сравнения TBB и OpenMP проверьте этот ответ

Ответ 2

Вы пишете код, используя только дополнительные потоки 'k', но это не оптимизировано для нескольких потоков.

Основной поток выполняет цикл for, ожидая завершения дополнительного потока, каждую итерацию цикла - т. blurSlowdown Этот код еще медленнее, чем делает blurSlowdown в одном потоке, потому что за создание потока blurSlowdown дополнительная плата, переключая контексты потока с помощью операций. ядро системы и синхронизация потоков происходит с помощью метода join().

К сожалению, стандартная библиотека C++ еще не имеет пула потоков (до C++ 20). Вы можете создать свой собственный, используя std::vector<std::thread> но лучше взять его из стандартной библиотеки donor - boost.

В любом случае, сегодня вы можете использовать OpenMP как:

#ifdef _OPENMP
#   include <omp.h>
#endif
....

#ifdef _OPENMP
#pragma omp parallel for
#endif // _OPENMP
for (int k = 0; k < 100; k++) {
   blurSlowdown(nullptr);
}

Другой вариант - сделать всю линейную алгебру на GPU (видеокарте), используя OpenCL, который параллелен по умолчанию.