软件发布前的优化与裁剪:gflags和glog

因为最早期的框架如Caffe,先支持了X86平台如CUDA等,在设计之初并未考虑现如今的移动端场景,能怎样简化开发流程,有什么现成的库就拿来用。这也导致早期安装Caffe时,由于一堆依赖需要预先安装,相当费劲。

后来有了移动端的推理需要,Github上便有人对Caffe进行裁剪,比方下面这两个项目:sh1r0/caffe-android-lib: Porting caffe to android platformsolrex/caffe-mobile: Optimized (for size and speed) Caffe lib

2017年中,看到过百度的手百团队
《利用CNN实现无需联网的图像识别》的移动端IOS和安卓的AI报告,针对移动端内存、耗电量、图搜插件100KB大小限制、模型10MB大小限制、模型部署加密等问题,提出并落地了相关解决方案。 不过早期的思路,便是精简Caffe,比方做的工作就有:blas从openblas切换到cblas;裁剪glog;精简gflag;裁剪protobuf;移除backpropagation;并保留常见Operator,并对部分模块Blob,Net等进一步精简,Caffe原本37MB降为100KB大小。

移动端本身就够复杂了,因而有简化甚至是移除这些依赖库的要求,甚至为此开发新的框架。接上次发的《软件发布前的优化与裁剪:初识》,上次讲到我们可以在cmake ..后追加--graphviz=这个参数来打印编译的target依赖,以此作为裁剪的参考,还有strip命令等。这次记录下裁剪、移除gflagsglog等库的具体实践和想法。目录如下:

  1. 简化glog
  2. 移除gflags
  3. 裁剪protobuf
  4. CMAKE中的CMAKE_BUILD_TYPE
    1. CMAKE_BUILD_TYPE的设置方式
    2. CMAKE_BUILD_TYPE的四种模式

1. 简化glog

考虑到移动端对动态、静态库的轻量要求,比方ncnn-andorid-armv8静态库仅1.9MB,裁剪glog。其实说裁剪并不准确,实际中重新实现了有如下功能的logging

# 支持的函数:其实就是用到的glog中的函数(等效实现)
CHECK
CHECK_EQ
CHECK_NE
CHECK_LT
CHECK_LE
CHECK_GT
CHECK_GE

LOG(INFO)
LOG(WARNING)
LOG(ERROR)
LOG(FATAL)

VLOG(0~9): enable by `export GLOG_v=<GLOG_v_number>` in command line.

logging支持和glog一样的NDEBUG的宏,来关闭所有检查和日志打印。在实现过程中,也发现交叉编译NDK工具提供的库对std::cerrstd::cout的功能支持并不完全(出现segfault),所以转而使用fprintf(stderr)来代替。

轻量级的logging实现基于宏函数和C++类,举个LOG(INFO)的例子:

#define LOG(status) LOG_##status.stream() #define LOG_ERROR LOG_INFO #define LOG_INFO LogMessage(__FILE__, __FUNCTION__, __LINE__, "I") 
class LogMessage {
 public:
  LogMessage(const char* file, const char* func,
             int lineno, const char* level="I")  {
    gen_log(log_stream_, file, func, lineno, level);
  }

  ~LogMessage() {
    log_stream_ << '\n';
    fprintf(stderr, "%s", log_stream_.str().c_str());
  }

  std::ostream& stream() { return log_stream_; }

 protected:
  std::stringstream log_stream_;
  LogMessage(const LogMessage&) = delete;
  void operator=(const LogMessage&) = delete;
};

上面代码省略了gen_log函数的实现,感兴趣可以去看我分拆出的例子logging: A light-weight C++ logging for mobile.

裁剪完成后再通过cmake .. --graphviz=./target命令查看依赖关系是否去除了glog,新的logging是否在。

后来想想,为了极致性能,还可以这样写:

#include <iostream> 
#define LOG(DEBUG, __FILE__, __FUNCTION__, __LINE__) \   if (DEBUG) \
    fprintf(stderr, "%s %s %d\n", __FILE__, __FUNCTION__, __LINE__); \
  else \
    ;

int main(int argc, char** argv) {
  int DEBUG = 1; // 控制打印是否生效   LOG(DEBUG, __FILE__, __FUNCTION__, __LINE__);
  return 0;
}

2. 移除gflags

我发现项目中,有使用gflags的方法有:

DEFINE_string
DEFINE_bool
DEFINE_int32

DECLARE_string
DECLARE_bool
DECALRE_int32

gflags::ParseCommandLineFlags

其实就两类吧,上面几个都好实现。瞎写了下面的实现方式:

#include <string> #include <iostream> 
#define DEFINE_string(var, default_val, note) static const std::string FLAGS_##var(default_val) #define DEFINE_bool(var, default_val, note) static const bool FLAGS_##var(default_val) #define DEFINE_int32(var, default_val, note) static const int32_t FLAGS_##var(default_val) 
#define DECALRE_string(var) static std::string FLAGS_##var #define DECALRE_bool(var) static bool FLAGS_##var #define DECALRE_int32(var) static int32_t FLAGS_##var 
int main(void) {
  DEFINE_string(model_name, "123", "model name");
  std::cout << "=== DEFINE_string ===" << std::endl;
  std::cout << "model_name:" << FLAGS_model_name << std::endl;
  return 0;
}

但就是gflags::ParseCommandLineFlags这个不太好搞,我看了下有类似的C++命令行参数解析的如getoptC/C++ 命令解析:getopt 方法详解和使用示例。但getopt的实现出来的和gflags::ParseCommandLineFlags这种还是不同,gflags是一开始用如DEFINE_string,定义好了全局的变量,之后命令行传参。命令行的参数的名称(不是值)和一开始DEFINE_string的变量名要匹配,这个不知道怎么实现,不过可以直接通过如argv[0]argv[1]来传给DEFINE_string定义的全局变量,这样就简单多了(或者是结合getopt)。

但是后来根据打印出来的依赖图发现,这些gflags的方法只有在单元测试中才有。所以不用管gflags的裁剪问题了。此外,我也看了ncnn、mnn、mace是否使用了gflags,发现他们即使有使用也是在benchmarktestexample中有使用。

3. 裁剪protobuf

这里我没有实际去做,据说是对将protobuf的实现摘出来。不过,这里记录下TensorFlow和Paddle-mobile的做法。

TensorFlow Lite使用的是FlatBuffers,在其文档中有提到:

TensorFlow Lite的解释器(Interpreter)使用的是压缩后的FlatBufferModel用于推理。其中,
FlatBuffers是一个跨平台、高效、开源的序列化库,类似
protocol buffers,但前者特点在其访问数据前不需要做解析或解压缩(parsing/unpacking)的步骤,避免了内存分配。

转换器(Convertor)的作用是将TensorFlow常见的四种格式(
SaveModels
freeze_graph.py生成的Frozen
GraphDef、tf.keras HDF5 models、tf.Session TF-Python-API generated)的模型转换成Lite的FlatBuffer格式
.tflite

Paddle-Mobile是将protobuf的库进行裁剪,只留下两个文件protobuf-c.hprotobuf-c.c,位于其项目根目录的src/protobuf-c/路径下。

4. CMAKE中的CMAKE_BUILD_TYPE

上次在我们《软件发布前的优化与裁剪:初识》这篇文章中,提到命令strip,以及对应的gcc中的-s参数,和cmake设置(这个也要提前手动设置好strip命令,见一篇文章中的CMake小节)。

这两天发现CMAKE_BUILD_TYPE其实也可以做到裁剪库,根据CMake最新的文档对CMAKE_BUILD_TYPE的介绍

CMAKE_BUILD_TYPE选项用来指定单个配置生成器(single-configuration generators)如
Makefile生成器
Ninja生成器的构建类型,多配置生成器(multi-configuration generators)的构建类型是不行的。设定后,会静态地在构建树(build tree)中,按照指定的构建类型构建。

CMAKE_BUILD_TYPE的可能取值:空、
Debug
Release
RelWithDebInfo
MinSizeRel

每个属性和变量都是配对的,即
SOME_VAR_<CONFIG>这样的形式,比方
CMAKE_C_FLAGS_<CONFIG>,指定为大写形式,即
CMAKE_C_FLAGS_[DEBUG | RELEASE | RELWITHDEBINFO | MINISIZEREL | ...]。其中,比方
CMAKE_BUILD_TYPE选的是
Debug模式,那么对应在编译C语言时,编译选项就会根据
CMAKE_C_FLAGS_DEBUG来编译。

4.1 CMAKE_BUILD_TYPE的设置方式

设置CMAKE_BUILD_TYPE的方法,可以通过命令行的cmake命令设置,也可通过CMakeLists.txt指定。

# 方式一:命令行设置CMAKE_BUILD_TYPE
$ mkdir Release  
$ cd Release  
$ cmake -DCMAKE_BUILD_TYPE=Release ..  
$ make 

# 方式二:在`CMakeLists.txt`里设定
SET(CMAKE_BUILD_TYPE "Debug”)
# 或
SET(CMAKE_BUILD_TYPE "Release")

4.2 CMAKE_BUILD_TYPE的四种模式

CSDN一篇博客《Visual Studio之RelWithDebInfo模式,“被忽视”的编译模式》讲到:在Visual Studio的编译模式选项中,一般有四个模式:DebugReleaseRelWithDebInfoMinSizeRel

  • Debug版本是调试版本,对代码不做任何优化,可以debug项目中的任意文件;
  • Release版本是发行版本,顾名思义就是当程序开发完成后,程序的一个发布版,它对代码做了优化,因此速度会非常快,但是遗憾的是,release版本无法跟踪代码。

StackoverFlow上有个问题:cmake – What are CMAKE_BUILD_TYPE: Debug, Release, RelWithDebInfo and MinSizeRel? – Stack Overflow进一步解释了,Debug版本为了追求更全面的调试信息而放弃了速度,而Release版本为了追求性能优化而抛弃了调试信息。若既需要调试代码(具有调试debugging用的符号文件),又希望加快速度,RelWithDebInfo这个版本就满足。

MinSizeRel版本与Release版本一样,只是该版本进一步优化了体积,并最大化了代码优化级别。

在生产环节中的构建,我们会选择Release版本。此外,最常用的两个选项一直都是ReleaseDebug

参考:

    原文作者:按时计划
    原文地址: https://zhuanlan.zhihu.com/p/73592450
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞