【问题标题】:Why cv::parallel_for_ run faster than my own implementation?为什么 cv::parallel_for_ 比我自己的实现运行得更快?
【发布时间】:2021-10-14 01:22:26
【问题描述】:

我正在为 RGB 图像实现最近邻域大小调整算法(无符号字符类型)。考虑到在 Android ARMv8 平台上与 OpenCV 的速度比较,我发现 OpenCV 使用cv::parallel_for_ 进行多线程加速。

于是,我钻研OpenCV的cv::resize()对应的源码,将实际运行的代码复制粘贴,放入我的main.cpp。它包含一个仿函数resizeNNInvoker,以及在这个仿函数上执行多线程计算的cv::parallel_for_

让我感到困惑的是cv::parallel_for_ 版本比使用my_parallel_for_ 运行得更快,其代码与OpenCV 保持一致。

为了更清楚:

  • 在 Android armv8 平台上测试
  • **用 OpenMP 多线程编译 OpenCV,转其他并行框架
  • 转到OpenCV的cv::parallel_for_,将其源代码更改为与my_parallel_for_相同(见下文)
  • cv::setNumThreads(4) 使用 4 个线程,绑定 4 个大 cpu 核心(使用 ncnn API)
  • 所有代码在 Release 模式下编译(通过 CMake)
  • 测试输入图片:width=7680,height=4320,目标图片尺寸:7680/3, 4320/3。

时间成本如下:

method time cost
cv::parallel_for_ 3.24 ms
my_parallel_for_ 7.67 ms
inplace openmp 7.75 ms
// my own implementation of parallel_for_, copied from OpenCV source code
void my_parallel_for_(const cv::Range& range, const cv::ParallelLoopBody& body)
{
    #pragma omp parallel for schedule(dynamic) num_threads(4)
    for (int i = range.start; i < range.end; ++i)
        body(cv::Range(i, i + 1));
}

// The functor that performs nearest neighbor resizing, copied from opencv source
class resizeNNInvoker : public cv::ParallelLoopBody
{
public:
    resizeNNInvoker(const cv::Mat& _src, cv::Mat &_dst, int *_x_ofs, double _ify) :
        ParallelLoopBody(), src(_src), dst(_dst), x_ofs(_x_ofs),
        ify(_ify)
    {
    }

    virtual void operator() (const cv::Range& range) const CV_OVERRIDE
    {
        //printf("--- resizeNNInvoker get called\n");
        cv::Size ssize = src.size(), dsize = dst.size();
        int y, x, pix_size = (int)src.elemSize();

        for( y = range.start; y < range.end; y++ )
        {
            uchar* D = dst.data + dst.step*y;
            int sy = std::min(cvFloor(y*ify), ssize.height-1);
            const uchar* S = src.ptr(sy);

            switch( pix_size )
            {
            case 1:
                for( x = 0; x <= dsize.width - 2; x += 2 )
                {
                    uchar t0 = S[x_ofs[x]];
                    uchar t1 = S[x_ofs[x+1]];
                    D[x] = t0;
                    D[x+1] = t1;
                }

                for( ; x < dsize.width; x++ )
                    D[x] = S[x_ofs[x]];
                break;
            case 2:
                for( x = 0; x < dsize.width; x++ )
                    *(ushort*)(D + x*2) = *(ushort*)(S + x_ofs[x]);
                break;
            case 3:
                for( x = 0; x < dsize.width; x++, D += 3 )
                {
                    const uchar* _tS = S + x_ofs[x];
                    D[0] = _tS[0]; D[1] = _tS[1]; D[2] = _tS[2];
                }
                break;
            case 4:
                for( x = 0; x < dsize.width; x++ )
                    *(int*)(D + x*4) = *(int*)(S + x_ofs[x]);
                break;
            case 6:
                for( x = 0; x < dsize.width; x++, D += 6 )
                {
                    const ushort* _tS = (const ushort*)(S + x_ofs[x]);
                    ushort* _tD = (ushort*)D;
                    _tD[0] = _tS[0]; _tD[1] = _tS[1]; _tD[2] = _tS[2];
                }
                break;
            case 8:
                for( x = 0; x < dsize.width; x++, D += 8 )
                {
                    const int* _tS = (const int*)(S + x_ofs[x]);
                    int* _tD = (int*)D;
                    _tD[0] = _tS[0]; _tD[1] = _tS[1];
                }
                break;
            case 12:
                for( x = 0; x < dsize.width; x++, D += 12 )
                {
                    const int* _tS = (const int*)(S + x_ofs[x]);
                    int* _tD = (int*)D;
                    _tD[0] = _tS[0]; _tD[1] = _tS[1]; _tD[2] = _tS[2];
                }
                break;
            default:
                for( x = 0; x < dsize.width; x++, D += pix_size )
                {
                    const uchar* _tS = S + x_ofs[x];
                    for (int k = 0; k < pix_size; k++)
                        D[k] = _tS[k];
                }
            }
        }
    }

private:
    const cv::Mat& src;
    cv::Mat& dst;
    int* x_ofs;
    double ify;

    resizeNNInvoker(const resizeNNInvoker&);
    resizeNNInvoker& operator=(const resizeNNInvoker&);
};

// The entry function that calls nearest neighbor resizing with openmp multi-thread
void resize_nearest(const uchar* src_buf, int src_height, int src_width, int src_linebytes, uchar* dst_buf, int dst_height, int dst_width, int dst_linebytes, const Option& opt)
{
    cv::Size src_size;
    src_size.height = src_height;
    src_size.width = src_width;
    cv::Mat src(src_size, CV_8UC3, const_cast<uchar*>(src_buf));

    cv::Size dst_size;
    dst_size.height = dst_height;
    dst_size.width = dst_width;
    cv::Mat dst(dst_size, CV_8UC3, dst_buf);

    cv::Size ssize = src.size(), dsize = dst.size();

    double inv_scale_x = (double)dsize.width/ssize.width;
    double inv_scale_y = (double)dsize.height/ssize.height;
    double fx = inv_scale_x;
    double fy = inv_scale_y;

    cv::AutoBuffer<int> _x_ofs(dsize.width);
    int* x_ofs = _x_ofs.data();
    int pix_size = (int)src.elemSize();
    double ifx = 1./fx, ify = 1./fy;
    int x;

    for( x = 0; x < dsize.width; x++ )
    {
        int sx = cvFloor(x*ifx);
        x_ofs[x] = std::min(sx, ssize.width-1)*pix_size;
    }

    cv::Range range(0, dsize.height);

    // !! define the instance of resizeNNInvoker functor.
    resizeNNInvoker invoker(src, dst, x_ofs, ify);

#if 0
    cv::parallel_for_(range, invoker);   //!! use opencv's, cost 3.24 ms
#elif 0
    my_parallel_for_(range, invoker);    //!! use own implementation, cost 7.67 ms
#else
    set_omp_dynamic(1);    //!! use inplace-implementation, cost 7.75 ms
    cv::Range stripeRange = range;
    #pragma omp parallel for schedule(dynamic) num_threads(4)
    for (int i = stripeRange.start; i < stripeRange.end; ++i)
        invoker(cv::Range(i, i + 1));
#endif
}

【问题讨论】:

  • 许多可能的原因。也许它没有使用 OpenMP。也许是,但它使用不同的参数。也许是别的东西。不可能说。需要调查。
  • docs.opencv.org/4.5.2/d7/dff/… 这里有一些信息。 TBB 可以在 ARM 上使用吗?
  • @Micka 我不熟悉 TBB。我认为我不需要弄清楚 TBB 是否可以在 ARM 上使用,因为 OpenMP 与 Android NDK C/C++ 编译器捆绑在一起,因此 OpenMP 可以在 X86 GCC/MSVC/Clang 和 Android NDK clang 上使用。
  • @ChrisZZ 的问题是 opencvs parallel_for 是否可以使用 TBB(以及链接中提到的其他技术)来达到您观察中实现的差异
  • @Micka 实际上的原因正如我刚才回答的那样,由于 OpenMP 错误的配置。我的可执行目标的预期行为,通常应该是以下之一:编译错误,或者编译并链接好并运行好。但是之前的配置错误,编译链接OK(没有错误,不是很正确),但是运行结果是没有openmp。

标签: c++ multithreading opencv compilation openmp


【解决方案1】:

我终于弄明白了。我在 CMakeLists.txt 中的 OpenMP 配置导致性能不匹配。

具体来说,我构建了依赖于静态库libplain.a 的可执行文件test,然后libplain.a 使用这些(前一个错误的)编译并链接到OpenMP:

find_package(OpenMP)
if(NOT TARGET OpenMP::OpenMP_CXX AND (OpenMP_CXX_FOUND OR OPENMP_FOUND))
    target_compile_options(plain PRIVATE ${OpenMP_CXX_FLAGS})
endif()

if(OpenMP_CXX_FOUND OR OPENMP_FOUND)
    if(ANDROID_NDK_MAJOR AND (ANDROID_NDK_MAJOR GREATER 20))
        target_compile_options(plain PRIVATE -fopenmp)
        target_link_libraries(plain PUBLIC -fopenmp -static-openmp)
    elseif(OpenMP_CXX_FOUND)
        target_link_libraries(plain PUBLIC OpenMP::OpenMP_CXX)
    else()
        target_link_libraries(plain PRIVATE "${OpenMP_CXX_FLAGS}")
    endif()
endif()

现在将所有PRIVATE 可见性更改为PUBLIC,然后openmp 编译和链接标志正确传播到可执行目标test

target_link_libraries(plain PUBLIC ${OpenCV_LIBS})

find_package(OpenMP)
if(NOT TARGET OpenMP::OpenMP_CXX AND (OpenMP_CXX_FOUND OR OPENMP_FOUND))
    target_compile_options(plain PUBLIC ${OpenMP_CXX_FLAGS})
endif()

if(OpenMP_CXX_FOUND OR OPENMP_FOUND)
    if(ANDROID_NDK_MAJOR AND (ANDROID_NDK_MAJOR GREATER 20))
        target_compile_options(plain PUBLIC -fopenmp)
        target_link_libraries(plain PUBLIC -fopenmp -static-openmp)
    elseif(OpenMP_CXX_FOUND)
        target_link_libraries(plain PUBLIC OpenMP::OpenMP_CXX)
    else()
        target_link_libraries(plain PUBLIC "${OpenMP_CXX_FLAGS}")
    endif()
endif()

有了这个更新的 cmake 配置、重建程序,my_parallel_for_ 的速度几乎与cv::parallel_for_ 相同。

【讨论】:

    【解决方案2】:

    这是来自 OpenCV 的用于选择实际线程框架的代码:

    #ifdef CV_PARALLEL_FRAMEWORK
    #if defined HAVE_TBB
    
    #if TBB_INTERFACE_VERSION >= 8000
            tbbArena.execute(pbody);
    #else
            pbody();
    #endif
    
    #elif defined HAVE_HPX
            pbody();
    
    #elif defined HAVE_OPENMP
    
            #pragma omp parallel for schedule(dynamic) num_threads(numThreads > 0 ? numThreads : numThreadsMax)
            for (int i = stripeRange.start; i < stripeRange.end; ++i)
                pbody(Range(i, i + 1));
    
    #elif defined HAVE_GCD
    
            dispatch_queue_t concurrent_queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
            dispatch_apply_f(stripeRange.end - stripeRange.start, concurrent_queue, &pbody, block_function);
    
    #elif defined WINRT
    
            Concurrency::parallel_for(stripeRange.start, stripeRange.end, pbody);
    
    #elif defined HAVE_CONCURRENCY
    
            if(!pplScheduler || pplScheduler->Id() == Concurrency::CurrentScheduler::Id())
            {
                Concurrency::parallel_for(stripeRange.start, stripeRange.end, pbody);
            }
            else
            {
                pplScheduler->Attach();
                Concurrency::parallel_for(stripeRange.start, stripeRange.end, pbody);
                Concurrency::CurrentScheduler::Detach();
            }
    
    #elif defined HAVE_PTHREADS_PF
    
            parallel_for_pthreads(pbody.stripeRange(), pbody, pbody.stripeRange().size());
    
    #else
    
    #error You have hacked and compiling with unsupported parallel framework
    
    #endif
    
            ctx.finalize();  // propagate exceptions if exists
            return;
    #endif // CV_PARALLEL_FRAMEWORK
    

    所以这是优先顺序:

    1. TBB任务竞技场
    2. 待定
    3. HPX
    4. OPENMP
    5. 苹果 GCD
    6. WINRT 并发
    7. Windows 并发
    8. PThread

    也许您的 opencv parallel_for 使用 TBB 而您的代码使用 OpenMP?

    不确定是否可行,但您可以尝试显式使用 opencv 中的 openmp,例如 C++ 中的 cv::parallel::openmp::parallel_for

    【讨论】:

    • 您粘贴的顺序是正确的,但是,我尝试的是,我将openmp的实现部分放在cv::parallel_for_的请求中,而不是保持原始状态。我还尝试删除 parallel_for_parallel_for_impl 中的所有其他后端宏确定和实现,但仍然比 my_parallel_for 快。
    • 如果您已经在更改openCV源代码并重建库,您可以在1.验证openmp代码行2.提取openmp设置中添加一些调试代码吗?
    • 是的,在我问这个问题之前,我添加了printf代码来验证openmp代码是否正在运行,已经执行了已经验证过的openmp代码行,然后我问这个问题。我不知道你是什么意思提取openmp设置,在我问这个问题之前,我阅读了opencv源文件,注意到一个从函数获得的静态变量,在这个函数内部,set_omp_dynamic(1);被调用,因此在我粘贴的就地调用 openmp 部分,我明确调用它。
    • 例如使用的线程数。您调用 #pragma omp parallel for schedule(dynamic) num_threads(4) OpenCV 调用 #pragma omp parallel for schedule(dynamic) num_threads(numThreads > 0 ? numThreads : numThreadsMax) numThreads 和 numThreadsMax 是哪些值?
    • 回复:致 Micka:setNumThreads 超出了我的时间测量区域。我一直把它放在外面,没有放在里面。
    猜你喜欢
    • 1970-01-01
    • 2014-03-12
    • 2019-11-01
    • 2019-06-28
    • 2013-11-21
    • 2012-07-13
    • 1970-01-01
    • 1970-01-01
    • 2019-01-31
    相关资源
    最近更新 更多