源
从当年流行的”编程高手必读Linux源码“,到市面上各色各样的XXX源码解析、剖析,我们已经看过太多太烂的源码分析。
读一份源码最痛之处在于,突然蹦出一大段代码,数据结构一个认不得,也不知道变量从何而来,函数更看都不懂。
似乎,那些很烂的作者,总喜欢迎难而上,你越不喜欢大段代码,他就每次首先贴一大段代码。
丝毫不对顺序做优化,也不知道标记出什么是重要的,什么是不重要的,连一份概况都没有。
这不叫源码分析,这叫随堂笔记。
高中时候,曾经拜读过侯捷老师的《深入浅出MFC》,其中最有趣的是它的第二章”MFC六大关键技术之仿真“。
这与许多写源码分析的作者大相庭径,既没有一大段代码,也不做什么概括。
它的”深入“精髓很简单,就是拆源码,将复杂、严谨的源码实现,结合自己的理解,以一种简洁、传神的方式仿真。
读这样的代码,是很轻松的,因为本来很复杂的源码,经过作者精心编排和二次改写后,看起来要和蔼可亲的多。
当然,我们不必去写书,但起码要学会拆源码,去仿真、简化、二次改写源码。
读一份复杂的源码,切记眼高手低。我在二次改写Caffe时,遇到的最大问题就是,尽管同一段代码看了许多遍,
每次理解竟然都不同,有错的,也有不足的,只有亲手编译、Debug之后,才发现,原来自己一直读”错“了。
”勿在浮台筑高楼“,这是《深入浅出MFC》这本书的精髓,其实也可以拓展到大部分源码分析问题上。
Caffe关键技术①·开放且先进的设计理念
1.1 引用库:瞧瞧你为什么配置Caffe要一天
Caffe是基于C++写成的,它不是C,它是严格的C++,严格到几乎是与C++ Primer提倡的,相似的现代C++编程风格。
C++标准库由ISO C++委员会严格审查控制,不像Java,所以它的内置库资源受到了严格的限制。
但这丝毫不会阻止C++的强大引用库的能力,但给配置这些库带来很大的麻烦。
广义上来说,Caffe引用的库分为如下几类:
1. 并行数学计算:cblas(CPU)、cublas(GPU)
2. GPU调度:CUDA
3. 本地数据库:LMDB/LevelDB
4、C++标准库扩展:Boost库(开源社区维护,史上最炫、最庞大的C++功能库集合)
5、日志管理与调试:Google GLog
6、执行命令解析:Google Gflags
7、图像处理与变换:OpenCV
8、数据结构设计,及高速序列化/反序列化:Google Protocol Buffer
9、参数扩展与可移植性:HDF5
这些库虽然你可能都没有用过,但是它们分别在处理相关任务上,速度绝对是No.1
当然,这些库有一个很不争的事实,和Google有撇不清的关系,而且Google还被墙了。这与作者Yangqing Jia的工作环境有关。
Caffe写于2013年末,作者2012年在Google MTV实习过,并且参与了Google Brain项目。
Google Brain项目的最大贡献,就是第一代大规模机器学习系统DistBelief(第二代为TensorFlow,已开源)
所以,Caffe就有了一个特殊性,它是学术界的人写的,但是这个人又精通工业界的那些技术。
我称之为,Caffe是一个半工业半学术级的框架。
你在Caffe的代码中,可以看到一些典型的工业级代码的思路和做法,比如 [严格的函数参数传入]:
Google内部对其程序员的代码,为了保持很好的可读性,特作以下规定:
void fun(std::string &str); 这样的函数我们很难分清楚str究竟是输入参数还是输出参数。 因此推荐这样写: |
这种代码风格,在Caffe中是严格执行的。
C++是严谨的语言,而良好的C++代码,则必须具有等量的严谨性。
1.2 CPU/GPU混合编程
Google Brain的DistBelief系统硬伤在于,它是基于分布式CPU节点计算的。
总负责人Andrew Ng在跳槽到Baidu之后,很懊悔自己当初的设计理念,他这样说道:
几年前我启动并主导了一个项目,当时还在谷歌,这个项目叫谷歌大脑。该项目利用谷歌的计算基础设施来构建神经网络。 规模大概比之前的神经网络扩大了一百倍,我们的方法是用约一千台电脑。这确实使深度学习取得了相当大的进展。用到相当多的计算机。不久之后我发现,之前我并没意识到,用一千台电脑是一项非常昂贵的技术。因此,我和我的朋友,意识到,利用一种不同的技术,仅用三台电脑,而非一千台,就可以做到这点,而秘诀就是利用GPU技术。 |
所以,Caffe在原始的设计理念上,就是以GPU为核心计算,CPU为辅助控制和I/O的这样框架。
由C/C++提供的编译宏功能,使得Caffe可以灵活的混合编程,仅仅添加一条宏,就能Build出不同平台需求的代码。
最新版Caffe,在CPU与GPU上,平衡的非常好。CPU多线程控制与I/O,GPU的多线程计算,两者水乳交融。
与之相对的是Theano,它几乎没有释放整台机器的全部计算力,无论你在其上二次封装什么样的库(Keras,Lasagne)
充其量也只是升级版的玩具,所以,Theano适合入门学习,但不要过度的依赖它,因为它实在太慢了。
1.3 数据结构
面向对象的设计理念有时是件麻烦事。
“类变量应该是私有的,仅且应当只由公有的方法提供接口,用于外部的控制”,Caffe依然严格的执行面向对象的设计原则。
这不是一个好主意,因为源码中会遍布着get、set方法,造成恶劣的不可读性。
所以Google提供了一个思路——将所有数据结构统一管理,以脚本形式快速定义,由机器自动生成冗繁的get、set。
这就是使用Google Buffer Protocol的第一个原因。
经典的系统级应用程序设计,对于数据的管理,必然要实现可持久化——频繁地在内存、硬盘之间进行数据交流。
这在应用程序设计中,称之为序列化、反序列化。
传统的序列化过程,需要程序员人工记忆序列顺序内容。当反序列化时,需要严格遵照顺序恢复,十分麻烦。
更重要的是,一般通用程序框架的反序列化的效率并不好(如Qt、MFC)
Google在Buffer Protocol中,使用了一种高效的编码方式,使得反序列化效率非常高,显著地增强了I/O能力。
这是使用Google Buffer Protocol的第二个,也是最重要的原因。
1.4 数据库
Google Buffer Protocol能够使得任意复杂的数据结构数据,快速编码为单个字符串,这就为“键-值”型数据库造桥铺路。
长期以来,应用级程序开发,通常会使用SQLite作为本地数据库(最典型的是安卓开发)。
SQL方式为数据的存储提供了方便,但同时它是慢的,慢到几乎不可以用于为高性能大数据计算提供I/O缓冲。
此时,Google 又开发了配套的快速 “键-值”型本地数据库LevelDB,跳过SQL,直接底层实现BST来存储。
LevelDB的I/O速度几乎是SQLite的几十倍,所以,早期的Caffe,数据封装由LevelDB负责。
在最新版Caffe中,广泛换成了LMDB。它多牺牲了一点内存空间用于缓存,却带来了比LevelDB更优
的I/O带宽。在如今内存贬值、白菜价的年代,LMDB显然受到主流追捧。毕竟I/O带宽,对于并行程序设计太重要了。
1.5 日志记录与调试
为程序的执行进行详细的日志记录,这是工业级代码的基本做法。
因为一旦程序崩溃了,作为主管,你不仅可以迅速定位位置、查找原因,还可以揪出隐藏在幕后的“背锅侠”。
Caffe中,几乎是遍布了日志记录代码,GLOG的简洁易用性,功不可没。
GLOG的日志记录分为四级,INFO、WARNING、ERROR、FATAL。
你可以通过 google::SetLogDestination 指定输出你要的日志级别,这四个级别是有趣的。
INFO级通常都是一些执行流程信息,WARNING级则是你需要注意的地方,但实际用的并不多。
ERROR级属于严重错误,但是并不会终止程序,作者无法确定接下来会发生什么事。
FATAL级属于致命错误,必须终止程序,这是不可争辩的事实。
FATAL级日志的实现,本质是封装了C/C++提供的assert(断言)功能。
assert代码编写过程中常用手段,GLOG利用assert,还定义了一些条件检查宏,如:
CHECK、CHECK_EQ,CHECK_LT,这些宏能够简化程序逻辑,给Debug带来方便。
毕竟,一个严谨的程序,需要杜绝任何以外情况。凡是估料到执行问题,都应当
使用CHECK宏加以断言标记,来保证程序的严谨性。
1.6 C++核心的一千零一夜
Boost库是C++最为激动人心的库之一,不仅仅是因为它编译完居然占用了3G空间,存在着一万多个引用的头文件。
最重要的是它提供了一些非常便利的功能,在Caffe中使用最广的,就是四大智能指针:
shared_ptr(全局自动释放)、scoped_ptr(局部自动释放)
weak_ptr(多线程安全访问)、thread_specific_ptr(多线程副本指针)
这些指针,让Caffe的代码秀气,安全,且充满灵活性。
其次,多线程程序设计一直是操作系统的兵家必争之地。现代操作系统的多线程功能,一般封装在操作系统内核中。
一些开发者会直接利用Linux内核提供的pThread进行代码设计,比如Tomas Mikolov的Word2Vec。
这给跨平台编译源码,带来了麻烦(很多人为了编译Word2Vec,不惜装Linux,或者找虚拟机)。
Caffe默认使用了Boost库的多线程功能,同Qt的设计理念一样,Boost库为Windows/Linux的内核。
以统一的接口,封装了多线程内核函数。这其实为Caffe for Windows的移植工作提供了便利。
除此之外,Boost库的多线程方案十分强大,足以设计大型的、安全的多线程应用程序。
1.7 数学计算
CBLAS与CUBLAS的函数接口几乎是一致的。
实际上,NVIDIA在设计CUBLAS的时候,是故意为之的模仿,减轻了CPU/GPU混合编程的代码混乱问题。
Caffe在底层封装了一些数学函数,也实现了一些高级的数学函数。
不要去死记硬背他们,也不要囫囵吞枣式的一步实现。
它们贯穿了Caffe的计算代码全部,所以,你在改写Caffe的初期,只需要选择实现其中的一小部分功能编程实现。