因为最早期的框架如Caffe,先支持了X86平台如CUDA等,在设计之初并未考虑现如今的移动端场景,能怎样简化开发流程,有什么现成的库就拿来用。这也导致早期安装Caffe时,由于一堆依赖需要预先安装,相当费劲。
后来有了移动端的推理需要,Github上便有人对Caffe进行裁剪,比方下面这两个项目:sh1r0/caffe-android-lib: Porting caffe to android platform 和 solrex/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
命令等。这次记录下裁剪、移除gflags
和glog
等库的具体实践和想法。目录如下:
- 简化
glog
- 移除
gflags
- 裁剪
protobuf
- CMAKE中的
CMAKE_BUILD_TYPE
CMAKE_BUILD_TYPE
的设置方式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::cerr
、std::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++命令行参数解析的如getopt
:C/C++ 命令解析:getopt 方法详解和使用示例。但getopt
的实现出来的和gflags::ParseCommandLineFlags
这种还是不同,gflags
是一开始用如DEFINE_string
,定义好了全局的变量,之后命令行传参。命令行的参数的名称(不是值)和一开始DEFINE_string
的变量名要匹配,这个不知道怎么实现,不过可以直接通过如argv[0]
、argv[1]
来传给DEFINE_string
定义的全局变量,这样就简单多了(或者是结合getopt
)。
但是后来根据打印出来的依赖图发现,这些gflags
的方法只有在单元测试中才有。所以不用管gflags
的裁剪问题了。此外,我也看了ncnn、mnn、mace是否使用了gflags
,发现他们即使有使用也是在benchmark
、test
、example
中有使用。
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.h
和protobuf-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的编译模式选项中,一般有四个模式:Debug
、Release
、RelWithDebInfo
、MinSizeRel
。
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
版本。此外,最常用的两个选项一直都是Release
和Debug
。
参考: