闲来无事,用VS2019编译了一个MNN工程,虽然官方提供了Windows编译的教程,但是实际上按照官方的步骤,编译出来的结果是一个MNN.lib的库和一堆exe,对于想要在windows下调试MNN或者利用VS2019的强大功能深入了解MNN非常不方便。这里介绍一下如何用VS2019编译MNN的windows工程。
首先,仍然需要按照官方的Windows编译的步骤,编译MNN。操作这一步,是因为有些第三方库需要编译,还有一些代码可能需要protobuf从proto编译为c的文件。这一步尽量在windows下操作,这样生成的库和文件才能满足VS的调用。当然,protobuf从proto编译c也可以在linux下进行。当然,如果你不打算把convertor也变成VS工程,那就不需要protobuf。也许大家要问我这是怎么知道的,其实都是当年编译NCNN的VS2019工程时候的一些血泪教训。当年用VS2019直接出发建立NCNN的工程就遇到过很多头文件不存在的问题,后来才发现,是因为需要首先把NCNN的库编译出来,这些头文件才会由cmake或者protobuf生成。如果你直接用windows下的CMake来根据cmakelists来生成工程,大概率也会失败,反而不如我这样来的方便直接。
需要注意一点,官方windows下编译推荐使用:
cd /path/to/MNN
powershell ./schema/generate.ps1
mkdir build
cd build
cmake -G "Ninja" -DCMAKE_BUILD_TYPE=Release ..
ninja
但是调用ninja的时候会报出一个莫名其妙的错误,我上官方的github也没有找到解决方式。最后发现在官方文档上,还有一种编译方法:
cd path/to/MNN
mkdir build
cd build
cmake -G "NMake Makefiles" -DCMAKE_BUILD_TYPE=Release -DMNN_BUILD_DEMO=ON ..
nmake
嗯,使用nmake就没问题了,至于是为什么,我不知道。
接下来的操作就很简单了,建立VS2019的MNN工程,把include和source两个目录下的所有h文件和cpp文件加入到工程中,加入的时候需要注意一下,一定要根据include和source的目录结构建立筛选器。source中也包含不少h文件,同样的要建立筛选器。最后建立完成之后,如下图所示:
MNN这个筛选器对应的就是include文件夹下面MNN文件夹,包含了里面所有的头文件。
需要注意的是schema目录下会有个current文件夹,这里面应该有一堆最开始编译MNN库时候生成的头文件,别忘记也加入工程。
cpp文件的处理同h文件的处理方式,同样需要注意按照目录结构建立筛选器。
这样操作完成之后,编译的时候依然会发现找不到头文件。不要着急,这个时候打开这个工程属性,如下图所示,把一些头文件引用库加入到引用目录中。
这个时候呢,你再编译,大概率没有问题了,不过这个时候的库依然是不能使用的。不信的话,可以尝试新建立一个工程,名字就叫demo,然后把MNN的demo/exec文件夹下的示例代码添加进去,如下图所示:
显然,这个demo工程会生成一个exe文件,需要依赖你刚才编译的MNN的库(这里我是用MTD,静态库的方式生成的MNND.lib)。这个时候就会报出LINK2019的错误,有一些函数没有链接进去,比如典型的MNNPackC4函数。
这是为什么呢?这里就不得不说MNN相比较NCNN来说,一个非常不友好的地方了。正常来说,NCNN到了这一步基本上编译肯定能通过,而且已经能够正常使用了。
为什么呢?这是因为NCNN里面用了很多windows自带的预处理定义,最典型的比如,如果你在VS2019工程属性中,配置使用增强指令集AVX,NCNN的代码里面直接就使用如下图所示方式打开AVX编译:
这是什么意思呢?也就是在VS2019中,如果你开启了AVX增强指令集,就会默认启用预处理定义__AVX__;SSE同理。
这样子,你可以随心所欲的选择是否启用增强指令集,总之依赖正确的预处理定义,NCNN在这块编译中一定不会出问题。
MNN这点就特别不好,比如下图:
MNN在x86_x64目录下,还有avx和sse文件夹,里面分别定义了启用增强指令集之后的一些函数。可是问题来了,MNN并没有正确的定义这些宏,而是使用MNN_USE_SSE这样的宏来替代,这就导致,如果你启用增强指令集之后,还需要手动添加预处理定义。这还不算完,最坑爹的是,avx和sse两个文件夹下都有个相同的CommonOptFunction.cpp文件,里面分别定义了avx和sse下应该如何用这两种不同的指令集处理相同的函数,并且,并且,没有任何的宏来进行编译控制。这就意味着,你如果选择了avx指令集,那么你就需要把sse这个文件夹里面的所有内容从你的工程中移除出去。
同时,在上图的compute目录下,MNN第三次定义了一个CommonOptFunction.cpp文件,这里面包含了没有任何优化时候该如何处理相同的函数。并且,有些函数,如MNNPackC4,是不论开不开启指令优化命令,都需要的一个函数,并且只在这个compute\CommonOptFunction.cpp文件中实现了该函数。这就导致一个很严重问题,就是不论你开不开启指令优化,你首先都需要把compute目录下的CommonOptFunction.cpp文件添加到工程中,这样才能保证如MNNPackC4这样只在该文件中实现的函数不会出现LINK2019的错误。接下来,如果你开启了优化,那么在avx或者sse中,还有一个CommonOptFunction.cpp文件,使得编译过程中产生一个警告:MSB8027。感兴趣的读者可以自行搜索。由于名字发生了冲突,这就导致LINK2019的错误依然存在。
我针对这个错误找了整整一个上午的解决方案,并且github上没有任何信息。好了,解决方式也很简单,重命名compute目录下的CommonOptFunction.cpp文件就可以了~惊不惊喜,意不意外?当然你也可以使用右键项目->属性->配置属性->C/C++->输出文件->对象文件名,将$(IntDir)改为$(IntDir)/%(RelativeDir)/。来解决同名的问题。
但是总而言之,从代码角度出发,我不认为MNN这样的代码命名方式是一个好的习惯。而不遵从标准预处理定义的模式也导致需要阅读代码才能添加相应的预处理定义,而不是只需要控制项目属性就可以了。这样子的操作,对于windows,特别是VS的无脑用户来说是不太友好的。
另外不得不提的一点是,NCNN编译的过程中,我从来没遇到过这样的问题:C4146:一元负运算符应用于无符号类型,结果仍为无符号类型。
这一问题是由于编译器SDL安全检查认为这一操作(通常是为无符号整形取负的操作)无效而产生的
这里我们假设定义一个数值为int INT,在32位机上面取值范围是-2147483648~+2147483647,
INT变量在后面会取负,比如Temp = -INT,
编译器认为INT有可能大于2147483647,那么会将INT优化为unsigned int类型,
SDL就可能认为我们的INT有可能取负了之后还是一样的,毕竟是unsigned 类型,因此提示错误。
当然,如果我们能确保自己的操作不会产生这个问题,这样子的操作也就不会有啥问题,可是你能确保一定不会产生这个问题?
但是MNN编译过程中就遇到了这个错误,解决方法当然也很简单,关闭sdl检查就可以了。问题是,掩耳盗铃的事情谁能确保?我想MNN在实际运行的过程中,一定有可能会遇到因为这个问题导致的错误发生,其实非常无语。
好了,到此为止,我们的MNN工程就算建立完了,接下来就可以正常编译demo,调试代码,部署自己的模型了。
不过,你们要是以为到此就结束了,那就未免太天真了。这个时候进行代码调试,十有八九会报出一个新的错误:我以官方指南上的姿态检测为例,调试结果如下:
MNN不支持卷积层??查看官方github,其实对于这个问题也有很好的解释,就是静态库在调用的时候,很多符号并没有强制链接,导致出现了明明已经定义的层却不支持。
https://github.com/alibaba/MNN/issues/805
不得不再次吐槽,MNN相比NCNN来说真的对windows太不友好了。
对于gcc来说,只需要增加--whole-archive链接选项即可保证调用静态库的时候强制链接所有符号。但是windows下该怎么办呢?对于VS2019工程来说,一个最接近的选项是/OPT:NOREF。
文档见:https://msdn.microsoft.com/en-us/library/bxwfs976.aspx
说明:/OPT:REF eliminates functions and data that are never referenced; /OPT:NOREF keeps functions and data that are never referenced.
/OPT:NOREF在Debug下是默认打开的,而且只能强制保留本工程未被使用到的函数和变量。对于引用的静态库的未被使用的函数和变量是不生效的。甚至有人认为这是微软的BUG在这个帖子里热烈讨论过:LINK.EXE BUG: /OPT:NOREF option doesn't work!
最后,找到了这么一篇帖子,解决了这个问题:
https://www.cnblogs.com/coderzh/p/LinkAllSymbols.html
大意就是,写一个python脚本,调用dumpbin,并且在输出结果中抽取出所有的符号名称,然后自动生成#pragma comment(linker, "/include:xxx")代码即可。
于是我按照他提供的方法进行了尝试,生成了一个LinkAllSymbols.h文件,这次终于成功了。
以官方示例为例,对于姿态检测的代码,multiPose.cpp,只需要在开头加上把上面生成的头文件include进来,就可以了。
最后附上VS2019工程的调试结果: