推理引擎设计技巧

1. 基于C/C++的基本优化

  1. 编译器很牛逼,GCC/CLANG 都有运行速度的优化选项,打开这些选项能帮助程序显著提升速度,虽然这还远远不够,但聊胜于无吧。

    下面是 ncnn 示例项目中的一段代码:

    # ncnn/examples/squeezencnn/jni/Android.mk
    LOCAL_CFLAGS := -O2 -fvisibility=hidden -fomit-frame-pointer -fstrict-aliasing -ffunction-sections -fdata-sections -ffast-math
    LOCAL_CPPFLAGS := -O2 -fvisibility=hidden -fvisibility-inlines-hidden -fomit-frame-pointer -fstrict-aliasing -ffunction-sections -fdata-sections -ffast-math
    LOCAL_LDFLAGS += -Wl,--gc-sections

    以前工作时候写的CMakeLists 中的代码:

    # set(CMAKE_BUILD_TYPE Release)
    if (CMAKE_BUILD_TYPE STREQUAL "Debug")
        set(CMAKE_CXX_FLAGS_DEBUG "$ENV{CXXFLAGS} -O0 -Wall -g2 -ggdb")
        message(STATUS "CMAKE_BUILD_TYPE = Debug")
    else()
        set(CMAKE_CXX_FLAGS_RELEASE "$ENV{CXXFLAGS} -O3 -Wall")
        message(STATUS "CMAKE_BUILD_TYPE = Release")
    endif()
  2. 书写高效的C代码。循环展开、内联、分支优化,避免除法,查表等优化小技巧要滚瓜烂熟,信手拈来。

  3. 必须看得懂汇编,即使你不写,也要知道编译器编译出来的汇编代码效率如何。这样你可以通过调整C/C++代码,让编译器生成你需要的代码。

2. 缓存友好

(1)少用内存
(2)连续访问、对齐访问、合并访问、显示对齐数据加载、缓存预取

3. 多线程

1. OpenMP

OpenMP会自动为循环分配线程,使用OpenMP加速只需要在串行代码中添加编译指令以及少量API即可。理想情况下,加速比大约能达到0.75*cores。

// ncnn/src/layer/relu.cpp
int ReLU::forward_inplace(Mat& bottom_top_blob, const Option& opt) const
{
    if (bottom_top_blob.elemsize == 1u)
        return ReLU::forward_inplace_int8(bottom_top_blob, opt);

    int w = bottom_top_blob.w;
    int h = bottom_top_blob.h;
    int channels = bottom_top_blob.c;
    int size = w * h;

    if (slope == 0.f)
    {
        #pragma omp parallel for num_threads(opt.num_threads)
        for (int q=0; q<channels; q++)
        {
            float* ptr = bottom_top_blob.channel(q);

            for (int i=0; i<size; i++)
            {
                if (ptr[i] < 0)
                    ptr[i] = 0;
            }
        }
    }
    else
    {
        #pragma omp parallel for num_threads(opt.num_threads)
        for (int q=0; q<channels; q++)
        {
            float* ptr = bottom_top_blob.channel(q);

            for (int i=0; i<size; i++)
            {
                if (ptr[i] < 0)
                    ptr[i] *= slope;
            }
        }
    }

    return 0;
}

​ 但并非所有循环都适合做多线程优化,如果每次循环只做了非常少的事情,那么使用多线程会得不偿失。实际运用中,可以通过 #pragma omp parallel for if (cond) 语句来判断runtime过程中是否要启用多线程。

动态调度
稀疏化
定点化
NEON 汇编

4. 内存精简

每个layer都会产生blob,除了最后的结果和多分支中间结果,大部分blob都可以不保留,开启ncnn的轻模式可以在运算后自动回收,省下内存。 如下图所示:

某网络结构为 Conv -> Bias -> ReLU -> Concat,在轻模式下,向ncnn索要Concat结果时,Conv结果会在运算Bias时自动回收,而Bais结果会在运算ReLU时自动回收,而ReLU结果会在运算Concat时自动回收, 最后只保留Concat结果,后面再需要C结果会直接获得,满足绝大部分深度网络的使用方式。ncnn 开启轻模式仅需要一行代码:

set_light_mode(true)

相关参考资料:

Three-optimization-tips-for-c

通过GCC学习OpenMP框架

OPEN MP官网


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!