OpenCV的HAL、cpu指令集优化

opencv优化函数效率的方法分两种:HAL和根据正运行在的cpu指令集,其中HAL的优先级要高于cpu指令集。

  • CALL_HAL。试图对某个函数进行HAL优化。如果能够优化,所在函数就执行结束,否则须要执行后绪的根据指令集优化。
  • CV_CPU_DISPATCH。试图对某个函数进行依据cpu指令集优化。一个cpu会支持多种可优化指令集,和此操作关联的CV_CPU_DISPATCH_MODES_ALL宏指明了尝试这些指令集的次序,所以效率越高的指令集前越靠前。ALL中的最后一个总是BASELINE,它表示以通常的cpu指令集执行该函数。
  • cmake会生成cpu指令集优化的相关cpp文件,编译这些cpp时,需要在文件层额外定义CV_CPU_DISPATCH_MODE=<xxx>。以AVX2为例,需定义CV_CPU_DISPATCH_MODE=AVX2。
  • 要是不编译cmake生成的cpu指令集优化相关cpp文件,理论上链接时会报unresolved external symbol。
  • 某种cpu指令集优化既不属于baseline features,也不属于dispatched features,编译出的opencv库将不会对该指令集优化,即使cmake自动生成了该集指令的相关cpp文件。

一、优化过程

cv::Mat mat1(cv::Size(4, 1), CV_32FC1), mat2(cv::Size(4, 1), CV_32FC1), mat3;
mat1.at<float>(0, 0) = 1;  mat2.at<float>(0, 0) = 1;
mat1.at<float>(0, 1) = 1;  mat2.at<float>(0, 1) = -1;
mat1.at<float>(0, 2) = -1; mat2.at<float>(0, 2) = 1;
mat1.at<float>(0, 3) = -1; mat2.at<float>(0, 3) = -1;
cv::phase(mat1, mat2, mat3, true);

执行以上语句会进入cv::hal::fastAtan32f,让以fastAtan32f为例,描述OpenCV对函数的优化过程。

modules/core/src/mathfuncs_core.dispatch.cpp
------------------------------------------ 
#include "mathfuncs_core.simd.hpp"
#include "mathfuncs_core.simd_declarations.hpp" // defines CV_CPU_DISPATCH_MODES_ALL=AVX2,...,BASELINE based on CMakeLists.txt content
 
void fastAtan32f(const float *Y, const float *X, float *angle, int len, bool angleInDegrees)
{
    CV_INSTRUMENT_REGION()
    CALL_HAL(fastAtan32f, cv_hal_fastAtan32f, Y, X, angle, len, angleInDegrees);
    CV_CPU_DISPATCH(fastAtan32f, (Y, X, angle, len, angleInDegrees),
        CV_CPU_DISPATCH_MODES_ALL);
}

让深入CALL_HAL。

#define CALL_HAL(name, fun, ...) \
{ \
    int res = __CV_EXPAND(fun(__VA_ARGS__)); \
    if (res == CV_HAL_ERROR_OK) \
        return; \
    else if (res != CV_HAL_ERROR_NOT_IMPLEMENTED) \
        CV_Error_(cv::Error::StsInternal, \
            ("HAL implementation " CVAUX_STR(name) " ==> " CVAUX_STR(fun) " returned %d (0x%08x)", res, res)); \
}
#define cv_hal_fastAtan32f hal_ni_fastAtan32f

inline int hal_ni_fastAtan32f(const float* y, const float* x, float* dst, int len, bool angleInDegrees) { return CV_HAL_ERROR_NOT_IMPLEMENTED; }

CALL_HAL逻辑是这样的:如果HAL能对fun进行优化,res=CV_HAL_ERROR_OK指示执行操作了,并结束所在函数,否则res=CV_HAL_ERROR_NOT_IMPLEMENTED。这里假设没有用HAL优化(HAL优化见下文的“四、HAL优化”),针对fun=cv_hal_fastAtan32f,CALL_HAL最后执行hal_ni_fastAtan32f,它就是空操作,但由于返回CV_HAL_ERROR_NOT_IMPLEMENTED,调用它的fastAtan32f会继续执行后面的CV_CPU_DISPATCH。在深入这宏前让介绍mathfuncs_core.dispatch.cpp中一开始就执行的两条include语句。

#include "mathfuncs_core.simd.hpp"
#include "mathfuncs_core.simd_declarations.hpp"

第一个include作用是两个,1)声明cpu_baseline版本,2)实现baseline版本,这是通过不定义CV_CPU_OPTIMIZATION_DECLARATIONS_ONLY宏,使得mathfuncs_core.simd.hpp包含实现。

第二个include也两个作用,1)声明特殊指令集版本(只是声明),2)定义一个叫CV_CPU_DISPATCH_MODES_ALL的宏。

mathfuncs_core.simd_declarations.hpp是一个cmake生成的文件
-----------------------------------------------
#define CV_CPU_SIMD_FILENAME "C:/ddksample/opencv/modules/core/src/mathfuncs_core.simd.hpp"
#define CV_CPU_DISPATCH_MODE AVX
#include "opencv2/core/private/cv_cpu_include_simd_declarations.hpp"

#define CV_CPU_DISPATCH_MODE AVX2
#include "opencv2/core/private/cv_cpu_include_simd_declarations.hpp"

#define CV_CPU_DISPATCH_MODES_ALL AVX2, AVX, SSE2, BASELINE

#undef CV_CPU_SIMD_FILENAME

CV_CPU_DISPATCH_MODES_ALL宏指示了每个模块实现了的指令集版本。它实现了四种指令集,AVX2、AVX、SSE2是特殊指令集,BASELINE则是cpu_baseline实现。

上面四个版本,cpu_baseline的函数已由第一个include实现,三种特殊指令集则由另外三个源文件实现,它们有着一样内容。

#include "precomp.hpp"
#include "mathfuncs_core.simd.hpp"

要编译这三个文件,必须在文件层定义一个叫CV_CPU_DISPATCH_MODE的宏,从而出来opt_AVX、opt_AVX2、opt_SSE2版本。

  • 文件:mathfuncs_core.avx.cpp。文件层的预定义宏:CV_CPU_DISPATCH_MODE=AVX。最后实现的函数:cv::hal::opt_AVX::fastAtan32f
  • 文件:mathfuncs_core.avx2.cpp。文件层的预定义宏:CV_CPU_DISPATCH_MODE=AVX2。最后实现的函数:cv::hal::opt_AVX2::fastAtan32f
  • 文件:mathfuncs_core.sse2.cpp。文件层的预定义宏:CV_CPU_DISPATCH_MODE=SSE22。最后实现的函数:cv::hal::opt_SSE2::fastAtan32f

有了这四个版本的fastAtan32f实现,让回到CV_CPU_DISPATCH,看它是如何调用这四个函数。以下这个宏展开后的伪代码。

CV_CPU_DISPATCH(fastAtan32f, (Y, X, angle, len, angleInDegrees), AVX2, AVX, SSE2, baseline)
{
	if (cv::checkHardwareSupport(CV_CPU_AVX2) {
		cv::hal::opt_AVX2::fastAtan32f(Y, X, angle, len, angleInDegrees);
		return;
	}
	if (cv::checkHardwareSupport(CV_CPU_AVX) {
		cv::hal::opt_AVX::fastAtan32f(Y, X, angle, len, angleInDegrees); 
		return;
	}
	cv::hal::opt_SSE2::fastAtan32f(Y, X, angle, len, angleInDegrees); return;
	cv::hal::cpu_baseline::fastAtan32f(Y, X, angle, len, angleInDegrees); return;

}

CV_CPU_DISPATCH_MODES_ALL宏决定了四个fastAtan32f调用次序。要没意外,CV_CPU_DISPATCH的最后两个总是__CV_CPU_DISPATCH_CHAIN_BASELINE, __CV_CPU_DISPATCH_CHAIN_END,后者是个空操作。

#define __CV_CPU_DISPATCH_CHAIN_END(fn, args, mode, ...)  /* done */

CV_CPU_DISPATCH中,为什么CV_CPU_AVX2、CV_CPU_AVX会有checkHardwareSupport,SSE2、cpu_baseline则没有,这就涉及到cv_cpu_config.h是如何分类baseline features和dispatched features。

 

二、cv_cpu_config.h、baseline features和dispatched features

cv_cpu_config.h由cmake生成,指示了要使用的优化设置。以下是一个例子,支持优化指令集:SSE、SSE2、SSE4_1、SSE4_2、FP16、AVX、AVX2。

// OpenCV CPU baseline features

#define CV_CPU_COMPILE_SSE 1
#define CV_CPU_BASELINE_COMPILE_SSE 1

#define CV_CPU_COMPILE_SSE2 1
#define CV_CPU_BASELINE_COMPILE_SSE2 1

#define CV_CPU_BASELINE_FEATURES 0 \
    , CV_CPU_SSE \
    , CV_CPU_SSE2 \




// OpenCV supported CPU dispatched features

#define CV_CPU_DISPATCH_COMPILE_SSE4_1 1
#define CV_CPU_DISPATCH_COMPILE_SSE4_2 1
#define CV_CPU_DISPATCH_COMPILE_FP16 1
#define CV_CPU_DISPATCH_COMPILE_AVX 1
#define CV_CPU_DISPATCH_COMPILE_AVX2 1

cv_cpu_config.h定义的cv_cpu_config.h分两种:baseline features,dispatched features。baseline features指的是那些“所有”cpu必须能支持人SIMD。一旦运行在的cpu不支持这种SIMD,app会因为非法而退出(初始收集到此cpu能支持哪些针令集后,就会调用checkFeatures检查是否支持了全部的baseline features)。dispatched features指的是那些会运行时判断是否支持的指令集,即用cv::checkHardwareSupport运行时检查cpu是否能支持这种SIMD。

这两种features有什么作用?——它们影响上面所述的指令集优化CV_CPU_DISPATCH的行为。对dispatched features,会先用cv::checkHardwareSupport进行判断,支持了才会调用执行函数。对baseline features则没有checkHardwareSupport,直接调用。为什么会有这种结果,让以“AVX”为例进行说明。

#if !defined CV_DISABLE_OPTIMIZATION && defined CV_ENABLE_INTRINSICS && defined CV_CPU_COMPILE_AVX
#  define CV_TRY_AVX 1
#  define CV_CPU_HAS_SUPPORT_AVX 1
#  define CV_CPU_CALL_AVX(fn, args) return (opt_AVX::fn args)
#elif !defined CV_DISABLE_OPTIMIZATION && defined CV_ENABLE_INTRINSICS && defined CV_CPU_DISPATCH_COMPILE_AVX
#  define CV_TRY_AVX 1
#  define CV_CPU_HAS_SUPPORT_AVX (cv::checkHardwareSupport(CV_CPU_AVX))
#  define CV_CPU_CALL_AVX(fn, args) if (CV_CPU_HAS_SUPPORT_AVX) return (opt_AVX::fn args)
#else
#  define CV_TRY_AVX 0
#  define CV_CPU_HAS_SUPPORT_AVX 0
#  define CV_CPU_CALL_AVX(fn, args)
#endif
#define __CV_CPU_DISPATCH_CHAIN_AVX(fn, args, mode, ...)  CV_CPU_CALL_AVX(fn, args); __CV_EXPAND(__CV_CPU_DISPATCH_CHAIN_ ## mode(fn, args, __VA_ARGS__))

当认为AVX是baseline时,即预定义宏CV_CPU_COMPILE_AVX,CV_CPU_CALL_AVX将总是执行opt_AVX::fn。当认为AVX是dispatched时,即预定义宏CV_CPU_DISPATCH_COMPILE_AVX,CV_CPU_CALL_AVX将先用checkHardwareSupport(CV_CPU_AVX)进行判断,true后才执行opt_AVX::fn。如果AVX既不是baseline,也不是dispatched,CV_CPU_CALL_AVX就是个空操作,也就是说编译出的opencv不会有AVX优化。

为让支持更多cpu,应尽可能低的定义baseline features。对_WIN32,当前就是SSE、SSE2。

 

三、cmake如何生成cpu指令集优化的相关文件

以mathfuncs_core.dispatch.cpp为例,看相关过程。

mathfuncs_core.dispatch.cpp位在<opencv>/modules/core/src目录,打开该目录下的CMakeLists.txt。

<opencv>/modules/core/CMakeLists.txt
ocv_add_dispatched_file(mathfuncs_core SSE2 AVX AVX2)
ocv_add_dispatched_file(stat SSE4_2 AVX2)
ocv_add_dispatched_file(arithm SSE2 SSE4_1 AVX2 VSX3)
ocv_add_dispatched_file(convert SSE2 AVX2 VSX3)
ocv_add_dispatched_file(convert_scale SSE2 AVX2)
ocv_add_dispatched_file(count_non_zero SSE2 AVX2)
ocv_add_dispatched_file(matmul SSE2 SSE4_1 AVX2 AVX512_SKX)
ocv_add_dispatched_file(mean SSE2 AVX2)
ocv_add_dispatched_file(merge SSE2 AVX2)
ocv_add_dispatched_file(split SSE2 AVX2)
ocv_add_dispatched_file(sum SSE2 AVX2)

上面指出了core要优化哪些cpp。第一个ocv_add_dispatched_file对应mathfuncs_core.dispatch.cpp,指示mathfuncs_core.dispatch.cpp可进行SSE2、AVX、AVX2这三种优化。

在<opencv>目录查找使用了ocv_add_dispatched_file的地方,没找到哪个cpp要优化NEON指令集。对Android,v4.5.1应该没使用neon优化。

 

四、HAL优化

HAL优化过程请阅读网络一篇文章:“OpenCV中的HAL方法调用流程分析[1] ”。

到这里,我们发现该函数(hal_replacement.hpp定义的hal_ni_resize)直接返回CV_HAL_ERROR_NOT_IMPLEMENTED,按照上面的分析,hal::resize继续往下执行。那么,hal的实现是怎么切入进来的呢?

我们发现,hal_replacement.hpp中的CALL_HAL宏上有一句#include "custom_hal.hpp",好奇怪,include不一般都放在开头嘛?然后我们看下这个custom_hal.cpp,发现它只有一句#include "carotene/tegra_hal.hpp",我们继续跟踪下去。因为前面分析的函数为hal_ni_resize,直接find hal_ni_resize,没有结果。然后我们find cv_hal_resize,发现有:

<opencv>/3rdparty/carotene/hal/tegra_hal.hpp
---------
#undef cv_hal_resize
#define cv_hal_resize TEGRA_RESIZE

顿时就感觉快打通了,这里竟然把cv_hal_resize给undef掉了,我们知道在hal_replacement.hpp中是#define cv_hal_resize hal_ni_resize的,并且从文件的位置来看,这个def就会被undef掉,然后重新定义为TEGRA_RESIZE

custom_hal.hpp由cmake生成,以下是Rose在用的整个custom_hal.hpp。

#ifndef _CUSTOM_HAL_INCLUDED_
#define _CUSTOM_HAL_INCLUDED_

#if defined __ARM_NEON__ || defined __ARM_NEON
#include "carotene/tegra_hal.hpp"
#endif

#endif

__ARM_NEON__、__ARM_NEON,针对arm架构,Android、iOS都是预定义。因而总的是思路是:Android、iOS会使用carotene优化,windows则没有HAL。

要使用HAL优化,除custom_hal.hpp需include tegra_hal.hpp,还须要做以下事。

  • 把<opencv>/3rdparty/carotene/hal/tegra_hal.hpp复制到<opencv>/3rdparty/carotene/include/carotene。目的是为了让custom_hal.hpp能打开tegra_hal.hpp。
  • 预定义宏:-DCAROTENE_NS=carotene_o4t。目的是把<carotene>/src/*.cpp编译出的函数放入carotene_o4t这个命名空间,否则会被放入carotene。Opencv建议把这个宏定义放到全局。Rose考虑到全局影响太大,改为修改definitions.hpp文件。
    <opencv>/3rdparty/carotene/include/carotene/definitions.hpp
    ------------------
    #ifndef CAROTENE_NS
    #define CAROTENE_NS carotene
    #endif
    改为
    #ifndef CAROTENE_NS
    #define CAROTENE_NS carotene_o4t
    #endif
  • 预定义宏:-DWITH_NEON。Opencv建议把这个宏定义放到全局。Rose考虑到全局影响太大,改为修改CAROTENE_NEON宏的定义条件,去掉WITH_NEON。
    #if defined WITH_NEON && (defined __ARM_NEON__ || defined __ARM_NEON)
    #define CAROTENE_NEON
    #endif
    改为
    #if defined __ARM_NEON__ || defined __ARM_NEON
    #define CAROTENE_NEON
    #endif

carotene优化的效率怎样?——以cv::resize为例,使用、不使用cartonene的测试结果。硬件平台是firefly AIO3399J。

------------android, 使用cartonene-----------------
scale_surface from (4032x3024) == > (2016x1512), used: 19 ms
scale_surface from (4032x3024) == > (1344x1008), used: 37 ms
scale_surface from (4032x3024) == > (1008x756), used: 24 ms
scale_surface from (4032x3024) == > (1511x1311), used: 47 ms
scale_surface from (4032x3024) == > (2345x1789), used: 49 ms
scale_surface from (4032x3024) == > (4123x3123), used: 120 ms
scale_surface from (4032x3024) == > (5123x4123), used: 185 ms

------------android, 不使用cartonene-----------------
scale_surface from (4032x3024) == > (2016x1512), used: 14 ms
scale_surface from (4032x3024) == > (1344x1008), used: 46 ms
scale_surface from (4032x3024) == > (1008x756), used: 23 ms
scale_surface from (4032x3024) == > (1511x1311), used: 81 ms
scale_surface from (4032x3024) == > (2345x1789), used: 105 ms
scale_surface from (4032x3024) == > (4123x3123), used: 228 ms
scale_surface from (4032x3024) == > (5123x4123), used: 309 ms

小图像,没优势;图像越大,cartonene效果越明显。

 

五、visual studio是如何生成cv::hal::opt_AVX2::fastAtan32f函数实现

mathfuncs_core.avx2.cpp。文件层的预定义宏:CV_CPU_DISPATCH_MODE=AVX2。最后实现的函数:cv::hal::opt_AVX2::fastAtan32f

1、预定义宏:CV_CPU_DISPATCH_MODE=AVX2

图1 文件层定义宏:CV_CPU_DISPATCH_MODE=AVX2

这个宏不能定义为全局宏,单独在文件层定义。“Solution Explorer”找到mathfuncs_core.avx2.cpp,“鼠标右键”,弹出菜单中选“Properties”,“Preprocessor”,定义宏“CV_CPU_DISPATCH_MODE=AVX2”。参考图1。

2、mathfuncs_core.simd.hpp

cmake生成的mathfuncs_core.avx2.cpp
------
#include "../modules/core/src/precomp.hpp"
#include "../modules/core/src/mathfuncs_core.simd.hpp"

第一个include和这里讨论没啥关系,进入mathfuncs_core.simd.hpp。

namespace cv { namespace hal {

CV_CPU_OPTIMIZATION_NAMESPACE_BEGIN

// forward declarations
void fastAtan32f(const float *Y, const float *X, float *angle, int len, bool angleInDegrees);
...

#ifndef CV_CPU_OPTIMIZATION_DECLARATIONS_ONLY
// 编译mathfuncs_core.avx2.cpp时不会定义宏CV_CPU_OPTIMIZATION_DECLARATIONS_ONLY,会进入这里,于是以下实现了cv::hal::opt_AVX2::fastAtan32f函数
void fastAtan32f(const float *Y, const float *X, float *angle, int len, bool angleInDegrees )
{
    CV_INSTRUMENT_REGION();
    fastAtan32f_(Y, X, angle, len, angleInDegrees );
}

#endif // CV_CPU_OPTIMIZATION_DECLARATIONS_ONLY

CV_CPU_OPTIMIZATION_NAMESPACE_END

}} // namespace cv::hal

宏CV_CPU_OPTIMIZATION_NAMESPACE_BEGIN定义在<opencv>/opencv2/core/cv_cpu_dispatch.h

#ifdef CV_CPU_DISPATCH_MODE
#define CV_CPU_OPTIMIZATION_NAMESPACE_BEGIN namespace __CV_CAT(opt_, CV_CPU_DISPATCH_MODE) {
#define CV_CPU_OPTIMIZATION_NAMESPACE_END }
#else
#define CV_CPU_OPTIMIZATION_NAMESPACE cpu_baseline
#define CV_CPU_OPTIMIZATION_NAMESPACE_BEGIN namespace cpu_baseline {
#define CV_CPU_OPTIMIZATION_NAMESPACE_END }
#define CV_CPU_BASELINE_MODE 1
#endif

由于在文件层定义了CV_CPU_DISPATCH_MODE=AVX2,于是扩展后的CV_CPU_OPTIMIZATION_NAMESPACE_BEGIN。

namespace __CV_CAT(opt_, AVX2) {

__CV_CAT也是个宏,它就是把两个参数以字符串的形式串起来。于是再次扩展后的CV_CPU_OPTIMIZATION_NAMESPACE_BEGIN。

namespace opt_AVX2 {

到此能得出个结论,这里的fastAtan32f声明在了cv::hal::opt_AVX2命名空间。这里也没定义宏CV_CPU_OPTIMIZATION_DECLARATIONS_ONLY,于是也是在这个mathfuncs_core.simd.hpp完成了cv::hal::opt_AVX2::fastAtan32f函数实现。

参考

  1. OpenCV中的HAL方法调用流程分析 https://www.cnblogs.com/willhua/p/12521581.html

全部评论: 0

    写评论: