当前位置:文档之家› 侯捷 - Boost 技术与应用

侯捷 - Boost 技术与应用

Technology

是个引人注目的年份,中国扩大航天探测、北京奥运……这一年C++社群也将有大事发

生:C++标准委员会预计于2008发布新标准2.0版,是继1998标准发布后的又一个大动作(此前标准委员会曾于2003发布一些小修订,无足道哉)。此刻,迈向2008的2007开春之际,正是C++程序员摩拳擦掌投入此件大事的好时机。而一旦开始关切这个事件,你一定会常常听到“Boost ”这个名称,这对在C++圈子讨生活以及常年关切C++发展的人并不陌生——即使你从来没有认识过它或只是雾里看花一知半解。本专栏将以十来篇文章引领你运用Boost 并透彻认识其技术,包括分析其关键源码。

Boost 的缘由与近况

Boost 可以说是个C++程序库开发社群与平台,最初由一群热心的C++标准委员会成员创立,目前已扩大到C++社群的数千名程序员。这个组织开发出来的众多程序库统称为Boost 程序库。由于其成员大多对C++发展具有重大影响力,所以Boost 程序库被业界公认为“准标准”程序库——其为“准标准”的威力由C++ Standard 2.0新纳入的14项组件(记录于TR1,后述)中有10项奠基于Boost 程序库可见一斑。

Boost 程序库的宗旨是成为一个可运作于现代操作系统(包括Unix 和Windows 及其各种变体)、经公开自由之同僚覆审(public free peer-reviewed )后的可移植、附源码、良好搭配C++标准程序库的一个C++程序库。它的软件和技术文件

都是免费的,在遵循Boost Software License (详见Boost 下载包内的LICENSE_1_0.txt )的原则下你可以使用、复制、显示、散布、执行、传输、衍生Boost 程序库源码。因此,使用Boost 程序库,除了学习成本,零成本!

认识Boost 程序库

“Boost 程序库”是个总称,内含众多大小不一的程序库,其目前文件所列清单共有69个。表1是Boost 文件所列的程序库分类,各分类涵盖内容有可能重复。略有涉猎者可从表1收一目了然之效,从未接触者亦可从中获得一些概念。

当我们想表现某个Boost 程序库时,

一般常用这种表示法(首字大写):Boost.Pool ,Boost.Array ,Boost.Regex

有时也可能直接写出以命名空间(boost::)为前缀的classes ,像这样(全部小写):boost::pool_object ,boost::array ,boost::regex

Boost 纵览、构建与安装

2008

Boost技术与应用系列文章1

C/C++ >>>

Boost 各程序库的容量差异极大,Boost.Conversion 很小,Boost.Graph 很大。下面挑几个有趣的程序库为各位做简介。

l Boost.Regex

这个程序库实践Regular Expressions (正则算式),用以解决大量的模式匹配(pattern-matching )问题,常用来处理大型字符串、搜寻未能精确指示之子字符串(i n e x a c t substrings )、根据某种格式将字符串切割为词法单元(tokenize a string )。C++程序员借助其它语言如Perl 、awk 、sed 以解决此类问题的时代终于可以过去了。

l Boost.Bind

这是std::bind1st 和std::bind2nd 的泛化,允许以一致的语法针对任何“类函数”(包括function pointers, function objects,member function pointers )进行自变量绑定,并经由嵌套(nested )达到复合功能。它不要求classes 提供所谓result_type,first_argument_type, second_argument_type 。它的出现消弭了ptr_fun, mem_fun, mem_fun_ref 的必要。它通常和STL 算法并用,亦往往和Boost.Function 并用。

l Boost.Any

这个容器可储存异类(heterogeneous types )元素,不似

STL 容器要求元素需为相同类型。

l Boost.Smart_Ptr

提供众多智能型指针,其中shared_ptr 最被广泛使用,带来reference counting 效果。

l Boost.Pool

提供一个池式内存配置器(pooled memory allocator ),在大量小型区块的需求情况下,可大幅度节省空间并缩短存取时间。

l Boost.Serialization

这个程序库解决了STL 容器的最大困扰与最严重缺陷。从此STL 容器可以像MFC 容器那般以一个简单动作就将整个容器内容写入(或读出)硬盘,像这样:

oa << data; (或 oa & data;)ia >> data; (或 ia & data;)

其中oa 和ia 是所谓archive ,而data 必须是个支持Serialization

(也就是运用Boost .Serialization 写成)的class object ,或者是个STL 容器或一般变数。

这个程序库对待STL 容器采用非侵入式(intrusive )设计,所以STL 容器完全不必修改就可获得这份serialization 大礼,非常令人兴奋。

Boost 程序库与TR1

目前已有10个Boost 程序库被收纳到C++标准委员会的Library Technical Report (TR1),为进一步成为 C++ Standard 2.0的一部分而准备。

预计即将来临的TR2会涵盖更多Boost 程序库。表2让读者回顾一下C++ Standard 1.0(C++98)纳入的标准程序库涵盖范围,表3是TR1清单。

TR1记录C++ Standard 2.0中以标准程序库扩充形式呈现的C++语言新特性。虽然C++ Standard 2.0尚未面世,但

TR1

表1:B oos t

程序库分类与涵盖内容

表2:C++ Standard 1.0(C++98) 的标准程序库主要成份

Technology

已不可能再有任何重大变动。面对容量巨大的Boost ,如果你心有余而力不足,首先应掌握TR1这一部分。Scott meyers 在其《Effective C++》3/e 中对此有这样的描述:“不熟悉TR1机能而却奢望成为一位高效的C++程序员是不可能的,因为TR1提供的机能几乎给每一种程序库和每一种应用程序都带来利益。”

Boost 下载、构建与安装

动手的时间到了。首先以任何搜寻引擎查找“Boost ”关键词,通常获得的第一个连结就是https://www.doczj.com/doc/2b637328.html,/,其首页便有下载链接,链接至https://www.doczj.com/doc/2b637328.html, 的特定网页。我从其中下载了boost_1_33_0.exe (另有多种版本及多种压缩格式),这是个自我解压缩档,安装的硬盘可自行指定如下:

执行后所有Boost 相关程序库被解压缩置于e:\boost_1_33_0,

获得的 \boost_1_33_0拥有以下子目录:

其中boost 子目录内含源码(主要是*.hpp ),doc 子目录内含技术文件(主要是*.html ),libs 子目录内含测试程序(*.cpp )及技术文件(*.html 和*.xms )。至关重要的Boost 技术文件就是从e:\boost_1_33_0\index.htm 进入,建议你为它做一个桌面快捷方

式,方便随时阅读。图1是Boost 技术文件的起始画面,进入其右上角所示的Libraries | Documentation 后获得图2。

接下来准备构建并安装(build and install )。Boost 程序库的构建设施采用Boost.Jam ,它是Perforce Jam 的一个延伸版。首先从先前所说的SourceForge 网页下载boost-jam-3.1.13-1-

ntx86.zip ,解压缩后得:

我们要的是其bjam.exe 。将它拷贝到先前所得的e :\boost_1_33_0,然后准备批处理如下,这份批次文件主要是将路径指向VC6编译器(其中所列的MFC 目录应可拿掉)。稍后另有VC7编译步骤。

表3:TR1清单(C++ Standard 2.0

的标准程序库扩充内容)

图1:B oos t

技术文件的起始画面

图2:Documentation

主画面

C/C++ >>>

Boost 程序库大多以header files *.hpp 呈现,但也有些与系统相依的程序库(如date_time, filesystem, serialization,thread ……)需构建出相关的.lib 或.dll 。Boost 的预设构建设施企图编译所有必要的Boost 程序库产生.lib 和.dll ,连同必要的*.hpp 安装至预设位置。Windows 系统中的预设位置是C:\Boost (可改,后述),其lib 子目录将放置*.lib 和*.dll 而其include 子目录将放*.hpp 。

现在,进入\boost_1_33_0并执行:

其中命令参数msvc 表示将以VC6为编译器,其它可能的设定包括:

l vc7表示Microsoft Visual C++ command-line tools from

Visual Studio .NET

l vc-7_1表示Microsoft Visual C++ command-line tools from

Visual Studio .NET 2003

l vc-7_1-stlport 表示Microsoft Visual C++ command-line

tools from Visual Studio .NET 2003 + STLPort

l borland 表示Borland C++

l kylix 表示Borland C++ for Linux (Kylix)l gcc 表示GNU GCC on Unix and Cygwin l ……(共28种可能)

第二个命令参数(install )也有他种可能设定。所有可能的参数及各种设定请参考技术文件的“Getting Started ”(图2最下)。例如你若不想安装至预设位置(先前说过Windows 系统中的预设位置是C:\Boost ),可添加参数及其设定如下:

又假设你只打算构建并安装regex 程序库,可添加参数及其设定如下:

执行bjam.exe 之后,编译器开始全力运转,硬盘喀喇喀喇作响。约莫一顿饭时间(你别吃太快),果然在指定位置产生\Boost 及其两个子目录lib 和include ,如图3。原先的 \boost_1_33_0也会多出一个bin 子目录,内含编译过程中的*.obj 及编译结果(*.lib 和*.dll ),但它们对将来编程时使用Boost 没什么影响。

请注意,新版Boost 在安装*.hpp 时并非直接复制到\Boost\include ,而是再加一个子目录表示版本号码,因此本例获得的子目录其实是\Boost\include\boost-1_33\boost ,其下才置放所有*.hpp 。图3右侧并展示构建后Boost.Regex 对应

的*.lib 和*.dll ,

这些程序库的命名自有其规则,图4是Boost.Datetime 在Unix 系统下构建成功后的命名示例。

若在Windows 系统下,被共享程序库(shared libraries ,亦即动态连结库)名称中不会有prefix ,改以扩展名区别import 、dynamic 和static 三大类程序库程序库:

l *.dll 表示dynamic library version.l *.lib 表示import library for the dll.l lib*.lib 表示static library version.

每一个程序库构建完成后应该获得18个这类程序库,例如Regex 获得:

Boost 的 *.hpp

读者想必对“Boost ”源码以 .hpp 为扩展名感到好奇。扩展名(程序库延伸名)传达的意义是程序库的类型(种类),“.h ”扩展名表示header ,意义很好,从C 开始便被使用并沿用至C++,但我们却因此分辨不出C headers 或 C++ headers (虽然C++可吃进C headers )。C++ Standard 的header 命名规则是“无扩展名”,这又不太对劲,没有传达出任何意义,我们因此必须检阅程序库内容才能知道其类型。“.hpp ”可以毫不模糊地标示出一个C++ header file ,并且不会对任何现存常规产生不好的影响。

图3:构建并安装(build and install

)后获得的结果

图4 :Boost.Datetime 在Unix

系统下构建成功后的命名示例

Technology

图5:以下为VC6设定Boost's lib 和include

路径

对大多数拥有此类独立程序库的Boost 程序库而言,只要应用程序含有相应的header(s),编译器就能自动连结该分离的lib 程序库,前提是你的编译器必须支持以下性质:

此性质在Microsoft Visual C++、Intel C++、Metrowerks C++和Borland C++都获得支援。此外,预设为静态连结,但你有选择,只要定义BOOST_WHATEVER_DYN_LINK 便可改为动态连结,或定义 BOOST_ALL_DYN_LINK 强迫所有Boost 程序库采用动态连结。相对地也可定义BOOST_WHATEVER _NO_LIB 或BOOST_ALL_NO_LIB 分别禁用上述两性质。

如果编程时你希望观察哪个Boost 程序库被连结,可定义 BOOST_LIB_DIAGNOSTIC ,那会造成每有Boost 程序库被连结,

auto-linking code 就吐出一个#pragma 讯息。以VC7构建与安装Boost

以个别编译器来构建Boost 并没有什么好逐一介绍的,但由于我个人十分关注的Boost.Serialization 以VC6构建失败,编译器提示该程序库所用之boost spirit package (用以加载XML archives )必须使用1.6版而非Boost 1.33.0自带的1.8版,所以我决定再以VC7构建一遍。VC7(来自Microsoft Visual https://www.doczj.com/doc/2b637328.html, )的客户众多,也许这里的额外说明对读者另有一番引导作用。

这里所需的boost 程序库和boost.jam 程序库都和先前所述相同,解压缩步骤也相同。准备开始构建了,我用的是VC7.1,来自Visual Studio 2003。为了使用其命令行(command line )编译器,我先在 Dos Box 中执行以下批处理,用以设妥MSVCDIR, PATH, INCLUDE, LIB 等环境变量:

D:\Program Files\Microsoft Visual Studio .NET 2003\Common7\Tools\Vsvars32.bat

然后进入\Boost_1_33_0 执行以下命令:

经过一顿饭时间,获得:

其中\Boost\lib 拥有194个程序库,比V C 6构建所得的140个程序库多出program_options 和serialization 程序库。

将Boost 应用于项目

构建并安装好Boost 于c:\boost (假设)后,若要以命令行编译应用程序,并用上Boost 程序库,可为LIB 环境变量添加c :\b o o s t \l i b ,并为I N C L U D E 环境变量添加c :\boost\include\boost-1_33。后者使得应用程序按规定含入Boost header 时:

编译器能够到c:\boost\include\boost-1_33\boost 中寻找regex .hpp ,而连结器能自动连结至(例如)c:\boost\lib\libboost_regex-vc6-s-1_33.lib 。

但是,真正开发应用程序时若还使用命令行编译器未免太过简单。即便只是做些Boost 小测试,也得到整合环境下做,毕竟整合环境提供的除错和追踪能力能够为我们省下许多时间,比纯靠人力追踪源码轻松多了。下面分别描述VC6和Visual Studio 2003中的环境设定。

VC6环境设定

最主要还是环境变量LIB 和INCLUDE 的设定。点选VC6的Tools/Options ,点选其中的Directories 附页如图5,便可逐一设定目标。

程序开发过程中,如果你以(例如)Word 开启Boost headers ,而编译前未关闭之,编译器会抱怨:Cannot open include file:'boost/xxx': Permission denied 。不过既然是在整合环境下,开发过程中我们对Boost 源码的

观察往往透过整合环境的文字编辑器进行,那就不会产生上述抱怨。另需注意Boost

图6:修改程序库的只读属

C/C++ >>>

码都是只读文件,若要修改需先去除只读属性——透过“文件管理器”在文件icon 上按鼠标右键便可于对话框的属性栏中取消或设定只读属性,如图6。

当你的项目用到Boost ,涉及的所有Boost headers 会出现在“FileView ”的 “External Dependencies ”中,如图7。如果将它们加入项目(在项目icon 上右键点选“Add file to project ”),就可以在“ClassView ”画面中看到这些headers 的成员,如图8。这在探索Boost 源码时很是便利。

图7这个项目所涉及的Boost headers ,出现在“External Dependencies ”中:

Visual Studio 2003环境设定

最主要仍是环境变量LIB 和INCLUDE 的设定。首先建立项目,然后右键点选项目icon 的“属性”,再于属性页左侧点选“连结器”,然后于右侧的“其它程序库目录”栏填入必要路径,如图9。而后点选属性页左侧选项中的“C/C++”,再于右侧的“其它Include 目录”栏填入必要路径即可,如图10。

至此,所有准备工作已经完成,可以开始使用Boost ,并探索其内部机制了。下期再细说——首先探索Boost.Pool 。

经验谈

在你充分并放心地运用Boost 之前,必须要经过一些测试。下面是我的测试环境管理经验,以为诸君参考。我要运用VC6的workspace 和projects ,也就是VC7的“方案”和“项目”。其上下关系是workspace 统辖并管理projects (“方案”统辖并管理“项目”)。以下只显示VC6画面,VS2003(VC7)有着十分对应的画面,虽然长像不尽相同,但很容易类推。

首先我建立一个专门用于测试Boost 的VC6 workspace (.dsw ),步骤是点选“File/New ”并点选“New ”对话框中的“Workspace ”:

而后回答问题,为它指定名称和所处路径。然后点选“Tools/Options ”

为它指定路径(见图5)。这些设定将被旗下所有projects 共享。一旦打算测试并剖析某个Boost 程序库,我便在这个workspace 下建立一个专属的project

(.dsp ),步骤是以鼠标右键点选VC6 FileView 窗口中的上述workspace ,并点选右键选单中的“Add New Project to Workspace ”:

而后挑选project 种类—通常我的测试以文字模式进行,所以我

总是选“Win32 Console Application ”。

随后为它指定名称和所处路径,以及回答VC6的询问,完成设定。projects 可各自设定LIB 和IN-CLUDE 路径,但以目前用途而言

无此必要。

此后在多重projects 中首先点选其一,再点选其右键选单中的“Set as Active Project ”,再点选“Add Files to Project ”,便可开始编写程序。

图9:Visual Studio2003

的“其它程序库目录”

图10:Visual Studio 2003的“其它Include

目录”

7

图8:Boost headers 的成员出现在“Class View

”画面中

Technology

Boost.Pool (上)

Boost技术与应用系列文章2

C/C++ >>>

干知名的大型C++程序库,如MFC, STL, Loki ,都有自己的内存管理机制,为的是能够以最快速最节

省空间的方式供应objects 所需的内存。也就是说这些程序库都有一套迥异于C++ new 操作符的内存分配与归还机制,这意味new 操作符必然存在某种改善空间。本文首先说明这一值得改善的空间(Why Pooled Allocation ),然后简介MFC,STL, Loki 如何改善(How Pooled Application ),最后详细介绍Boost 的实现与关键细节。

为什么需要Pooled Allocation

这个题目相当于探讨传统的C malloc()或C++ new 有什么缺点。欲分配一块内存,C 语言用的是malloc()函数家族,例如

欲创建一个对象,C++语言用的是new 表达式,例如:

new 表达式实际上分为三个步骤,其一分配内存,其二转型,其三调用构造函数:

如果编译器没能找到上述的Foo::operator new(),就会改而调用全局函数::operator new(),那是C++程序库提供的内存分配基本工具,功能相当于malloc(),这一点可从Borland C++源码获得清晰的印证:

由此可见,无论C 或C++,追根究底谈其内存分配时一定得接触malloc()。此函数的行为可从Doug Lea 所写的知名

版本窥知,此版本被各大编译器采用,源码见Doug Lea 的个人主页https://www.doczj.com/doc/2b637328.html,/dl/。追踪这份源码不是件容易的差事,我手上这份源码的.h 长达679行,.c 长达5621行,而且市面上找不到完整论述介绍其中的数据结构或算法。但纵使未能完完全全了解malloc()的行为,这里一定要让你知

道,malloc()分配出来的内存区块除了满足客户索求的大小,另附带了一个不为人知的区域,一般昵称为cookie (小甜饼),见图1。cookie 的大小和其储存的信息及格式,视不同的实现而有所不同。我曾做过一个试验,在刚开机的情况下

(此时可用内存的分布还很干净,未被乱切乱拼)分配若干区块,获得若干指针,然后将相邻各指针相减,这便假设是cookie 的大小(实验结果不是4就是8,取决于编译器实现)。再将每一指针的前4或前8个bytes 印出来,便是cookie 内容。但是不知其格式该如何打印呢?瞎猫碰死耗子,以整数格式打印看看。结果发现BCB 的cookie 总是(并且只是)记录着区块大小。其它编译器就没有这么“清纯”。

区块之前方带着一块“小甜饼”的必要性很容易理解:当我们将内存归还系统,我们调用的是free()(或?::operator delete())

并且只给予一个指针指向先前以malloc()获得的内存。这种情况下如果没有“小甜饼”帮助,free()根本不知道客户意欲释放的区块有多大。

cookie 甚至会记录区块大小以外的信息,例如该区块和其系统中的邻块之间的关系等,以利系统回收后做最佳安排。这些信息的设计取决于编译器实现。

为应付大小不一的各种需求,cookie 乃是必要之“恶”。但也就是这个cookie 为C++程序中常见的一种情况带来很大浪费:如果客户需要大量小区块(比方说100,000个对象,每个对象16 bytes ),那么每个区块都带cookie 就是一种浪费,因为大量小区块中的每一区块尺寸都相同,应该可以有更好的设计,不需要cookie 那份“区块尺寸”的记录。围绕这个概念的相关设计几乎全都基于一种相同的思考,就是Pooled Allocation (池式分配)。

为了拿掉cookie ,通常的做法是由管理器(程序库)挖一大块内存自行细切(不带cookie ,每一块尺寸都刚好符合客户需求)。这些被细切的小区块往往以linked list 管理,一整串小区块也就被称为free list 。当客户需要这种尺寸的小区块时,就由程序库从free list 中给出一个,并维护linked list 的正确链接。这种想法好似把一大块内存当成一个池子(pool )来供应“个头一致”的小东西,所以被称为池式分配。“pool ” 的另一个意义是“共享物”,当动词解则是“联营”,Pooled Allocation 的意思正是“联营系统下的内存分配”。

C++程序中,客户索求大量固定尺寸的小区块,这种情况很常发生(相当于制造出大量对象,每个对象都小小的)。也非常可能客户要的尺寸有三两种,因此一个好的 Pooled Allocation 设计,尤其是作为必须应付各种可能情况的角色而存在的程序库,最好能同时供应(维护)多个free lists ,每个list 负责供应某一特定大小的区块。Pooled Allocation 不但大量节省空间,亦巨幅节省时间,因为它大量减少了向系统

索求内存的次数。

如何设计Pooled Allocation

敏锐的你很快会想到,为了节省一个cookie (4或8个bytes )却赔上维护linked list 所需的指针(单向list 需要一个指针,双向list 需要两个指针),岂不是偷鸡不成蚀把米?但事实上我们不需要为每个区块各自准备指针,因为区块在尚未给出之前,也就是当区块还在吾人自定义的分配系统(程序库)的掌握下时,其空间可被程序库使用,对客户无任何影响,而这些空间(现实至少4 bytes )就足以做出至少一个

指针了。这种从区块本身挖空间建立指针的做法,在《Small Memory System, Patterns for system with limited memory 》(中

译本“内存受限系统之设计模式”)中称为“嵌入式指针”(Embedded Pointers )

,见图2。如图:给出的区块不再被free list 管理,未给出的区块前4 bytes 被当做维护linked list 所必须的指针。

这种设计可被相当精简地实现。《C++Primer, 3/e 》15.8节P.765和《Effective C++, 2/e 》条款10都列有对池式分配的片段举例,简单地说就是为各自例中的class 重载operator new 和operator delete (用以接管客户提出的申请),并在operator new 中管理单一free list 。两例的唯一差别是前一例未使用嵌入式指针,后一例使用了嵌入式指针,刚好让我们做出比较。

如果把上述功能独立出来放在一个分离而专用的class 内(假设名为Pool ),由它提供接口函数(通常名为allocate()和deallocate()),让客户的每个classes 重载operator new 和operator delete 并且其内不再自行管理free list 也不直接调用malloc()和free(),而是改调用Pool::allocate()和Pool::deallocate(),这就把池式分配干净地切割了出来,又让客户的classes 不知不觉用上它。这便是所有C++程序库池式分配系统的雏型。

下面我要描述的每一个C++程序库的内存分配系统,雏型都如以上描述。至于其各显神通的差异性,可从各张示意图中轻易感受。进行之前让我们统一术语的用法,在池式分配系统中,每次向系统要的大区块通常称为chunks ,chunk 内切割出来尺寸相同的小区块称为blocks 。STL, MFC, Loki 皆沿用这种称呼习惯,但Boost 刚好相反。讨论到Boost 时我会

图1:cookie

示意图

图2

:嵌入式指针

Technology

再次提醒你。此外,将内存还给程序库分配系统的动作,我会说归还(deallocation),将内存还给系统的动作,我会说释放或释还(free)。

Pooled Allocation各显神通

在广被大家遵循的策略如嵌入式指针、细切、operator new/delete重载之外,究竟Pooled Allocation还有什么戏法可变呢?关键在于以下数点:

l程序库该提供多少个free lists(每一个代表一种特定区块尺寸)才算合理?太少则用途不够广泛,而且这样的设计必须针对不提供的尺寸给客户一个说法。太多则过犹不及,会不会造成系统过于复杂?如何避免过于复杂?

l每个free list是真正一根肠子到底的单一linked list抑或是分段链接?如果使用单一长串linked list,将来要部分(甚或全部)释还给系统就不可得。要知道,Pooled Allocation 的策略是“先抢一大块再慢慢消化”,而接受归还时则是“纳入自己体系以便下次再响应需求”。如果从不考虑将区块释还给系统,程序就有可能因为取用大量内存而影响多任务环境下的其它进程(processes)对内存的使用。这不能说是内存泄漏(leak),因为还在程序掌控中,但它确实妨碍了其它进程。

l欲将区块释放给系统,不只是上述所说将free list分段就万事具备了。每一块内存,包括现在讨论的Pooled Al-location的一大块一大块内存,都必须保留cookie才能还给系统。如何能将每一大块内存的cookie都记下来而不混乱?

读者可以从下列讨论的各家系统看出各家的实现,真可谓戏法人人会变,各有巧妙不同。限于篇幅和主题,惟有谈到Boost时我才带引读者细看其体系结构和源码,其它程序库都只给出一个足以代表大局观的示意图,加上若干微言大义的文字。

SGI STL的std::alloc的实现

C++标准库(或说STL)中的allocator是个内存分配器,这个分配器在不同版本的STL上有不同的设计和不同的底层行为。本文要说的是SGI版本,事实上也只有它做出了Pooled Allocation。此分配器虽然也可被直接使用,但多半隐身幕后负责供应STL容器之内存需求。可想而知在现实程序中,使用SGI STL或使用其它版本的STL,当客户以容器放置大量元素时,内存用量和分配速度会有多么大的落差。

SGI STL allocator的class名为std::alloc,它一开始就(并且只)以一个array维护16个指针,用来指向16个free lists,分别处理8,16,24,32…(以8的倍数增加)至128 bytes的尺寸需求。对于不为8倍数或高于128 bytes的区块索求,它给的说法是,区块大小将被自动调整为8的倍数,超过128则意味cookie与区块尺寸之比例已经小到其浪费率可被忽略,因此改由系统内建的operator new/delete(相当于malloc/free)来服务。这个转向服务是隐晦的,客户并不知道(当然,现在我们都知道了),而且包装精美,完全不影响客户对区块的索求操作。

图3是SGI 的std::alloc完整示意图。此图非常珍贵(呵呵,我难免敝帚自珍),是历经许多测试并追踪源码后的结晶。std::alloc维护的16个指向free lists的指针系以static形式存在,一开始都是null。每当客户有所需求,std::alloc就找到对应的free list,如果其内有可用小区块就直接拿来用。一开始当然没有可用区块,于是向系统要20*2个区块(其实还有一个补充量,随着分配次数增加会愈来愈大。这里为了简化说明只说20*2。)那么大的空间,其中一半细切为20块,另一半留着以备下回再需向系统要内存时拿出来用(我称它为战备池)。战备池的存在是为了尽量减少向系统索求内存的次数,因为那很耗时。

基本上std::alloc就沿着这样的主轴进行。它的巧妙出现在当系统内存不足时的挖东补西能力和锱铢必较的精细程度。举个例子,当客户要求72 bytes,而对应的#8 free list无可用区块供应,按规则应取战备池来充填#8 list。如果战备池亦空,或不足(例如图3蓝色粗体字“24”就是战备池的当时大小),才向系统索求(此时战备池的余额会先挂到对应的free list中)。如果系统也弹尽源绝,std::alloc会依序寻找 #9—#15 lists看看有无可用区块。如果有,拿来切割放进#8供使用,余下的(可能是8,16,24,32…bytes)充填到对应的free list,吃干抹净丝毫不浪费。

这个系统有一个不太理想的地方:它很霸道,绝不释还内存给系统。事实上它无力归还,因为每次从系统分配来的一大块空间,并没有(也很难)将其cookie记录下来。这意味这一大块内存只有在程序结束后才能回归母亲的怀抱了。图3所示的#11和#12 free lists的第一个区块上方我画了一小块扁扁的东西,代表cookie,这个cookie所记录的大小有一部分被切割给其它free list用(因为战备池发挥功效之故),因此要保持cookie和其原始对应空间的关联,以及判断各free 图3:SGI STL std::alloc行为示意图

C/C++ >>>

lists 内的所有区块是否已全部回收,在此设计架构下是很不容易的事。

std::alloc 的用法如下:

你一定会惊讶,怎么归还时还需告知区块大小呢?这对客户的负担太大了。std::alloc 运用template 设计了一些包装,可有效为客户去除这层负担。

Loki::SmallObjAllocator 的实现

Loki 程序库对templates 的使用简直出神入化,它的前卫使它成为曲高和寡的踽踽先行者。不过,撇开其揭示的“以classes 表现设计模式”的高绝理念,以及前卫而惊世骇俗的template template parameters 技法,Loki 的“小型对象分配器”却是相对平易进人、功能充足而又设计轻巧。

图4是Loki 小型对象分配器的完整示意图。再一次,此图非常珍贵(呵呵,我又敝帚自珍了),同样是历经许多测试并追踪源码后的结晶,可收一目了然之效。从图中可以清楚看出,水平方向的SmallObjAllocator 作用相当于前述std::alloc 中维护16个free lists 的array ,但现在它不再是array 而是个vector ,从这里可以窥知它意图支持任意数量的free lists 。只要客户要求分配某对象,它便产生一个对应的free list 专门服务。图4显示目前制造出三个free lists 分别服务20,40,64三种尺寸。

图中垂直方向是身为f r e e l i s t 的第二层管理器FixedAllocator ,采“分段连续”设计,以vector 管理1…n 个大区块(所谓 “chunks ”)。所以第二层其实不是个linked list ,但我们仍可称它为free list 。大区块个中有特定数量的小区块(blocks ),最多255个,或最多令单一chunk 达到4K 。例如图中由左至右每一种chunk 分别切割为204、102、64个blocks 。每个block 等同于一个指定的对象大小,以最

底层管理器class Chunk 表现。Chunk 的设计很有意思,它相当于小型free list (也就是分段连续中的段),但它是以array 模拟linked list ,并以最前一个bytes 记录下一可用block 的索引,而不再采用嵌入式指针。这种算法挺少见,但十分经济有效。

这个设计的优点在于:第一,最上层和第二层都以vector 管理,所以不论free lists 或chunks 的个数都可以弹性增加,甚至减少——实际有无减少端视有没有能力将chunks 释还给系统。第二个优点是,chunks 内的blocks 的原个数、目前可用

个数、以及每个chunk 的起点地址都被记录了下来,前二者可据以判断某chunk 内的每一个blocks 是否都归还了,第三项(起点指针)则表示其前头必带着cookie ,那就是说它有能力将chunk 释还给系统。事实正是如此,在我所做的测试中,连续对同一尺寸要求多个blocks ,导致出现4个chunks ,而后一一归还,检测得知最后只剩2个chunks ——不全部释放,留下一些余地以备后用。Loki 的这个分配器比较王道,不复std::alloc 之霸道矣。

Loki “小型对象分配器”的相关classes 有四层:

l Chunk :这是最底层,相当于一小段free list 。实际乃

以array 模拟linked list 。

l FixedAllocator :这是中间层,相当于free lists ,以vector

串起一个个chunks 。

l SmallObjAllocator :这是最高层,以vector 管理众多free

lists 。

l SmallObj :这个class 架构于SmallObjAllocator 之上,主

要诉求是涉入Loki 的ThreadingModel 设计。它在内存分配上并无更多设计,所以本文也没有讨论它。

前三个classes 的关系如图5。

Loki 小型对象分配器的用法是:

图4: Loki::SmallObjAllocator 行为示意图

图5:Loki 分配器的三个主要clas ses

的关系

96

程序员

技 术

Technology

客户归还blocks 时必须告知大小。这对客户是一种负担。然而就像先前讨论std::alloc 时所说,运用template 可设计出一些漂亮的包装,为客户去除这层负担。

MFC's CFixedAlloc 的实现

图6是MFC CFixedAlloc 的示意图。MFC 的Pooled Allo-cation 主要由两个classes 担纲,一是CPlex ,一是CFixedAlloc 。前者扮演“分配chunk 并分段串接”的角色,后者内含前者并调用其函数,而后自行切割chunk 成为blocks 。

本图第一个chunk 的所有blocks 都被取光(白色),所以再要一个chunk (灰色)。每个chunk 带有一个CPlex 用来链接chunks 。

CPlex 只开放两个函数,Create()负责分配chunk ,而chunk 的大小等于block 个数×block 大小 + sizeof(CPlex )。其意图非常明显:由chunk 自身带着一个头部

(也就是一个CPlex )负责链接的必要数据,而链接动作就在Create()完成,但不细切。另一个开放函数是FreeDataChain(),将链接在一起的所有chunks (其blocks 尺寸都相同)以delete[] 释还给系统。但它不像Loki 会自行判断释还时机并自行采取行动,而是只在CFixedAlloc 析构函数中进行,也就是当此分配器离开作用域才进行,略逊Loki 一筹。

直接面对客户的是CFixedAlloc ,它内含一个指针指向CPlex ,并有一个开放函数Alloc(),其内调用CPlex::Create()以便获得chunk ,获得之后进行切割,采用嵌入式指针形成一个“blocks 数量固定(缺省为64)”的linked list ,而后给出一个block ,并以m_pNodeFree 指向下个可用的block 。细切所得的每个blocks 都以CNode 表现,那是定义于CFixedAlloc 内部的一个struct ,结构十分简单:

如果某次客户调用Alloc()时此分配系统无可用的blocks ,Alloc()内部就再调用CPlex::Create()取得一个chunk 并链接、

细切、给出block ,依此类推。

在这种设计架构下,MFC 分配器的用法是:

创建CFixedAlloc 对象并给予不同初值(代表block 尺寸)的次数如果是n ,就相当于建立起n 个分段链接的free lists ,每一段缺省64个blocks 。MFC 分配器的整体设计结构和Loki 比较接近。

图7是CPlex 和CFixedAlloc 的关系。有趣的是虽然MFC 有此分配器,但需要频繁运用分配器的容器(MFC 术语称之为Collection classes )如各种CxxxList 和 CMapxxxToxxx ,都不使用它而是直接在自己class 内也按着葫芦画瓢地做一遍CFixedAlloc 的工作,白白丧失了复用价值。

顺带一提,用过MFC 的程序员都知道MFC 特爱提供各种宏。由于为class 设计池式分配是如此标准化而制式,不外乎就是为该class 重载operator new 和operator delete 并在其中调用池式分配器提供的分配和归还函数(如上述的Alloc()和Free()),因此MFC 也为此设计了一套宏:DECLARE_FIXED _ALLOC 和IMPLEMENT_FIXED_ALLOC 。

MFC 程序员看到这两个名称一定露齿微笑。这组宏在CString 及CTempXXX 中被用到。

Boost.Pool 的实现

图8是Boost.Pool 的示意图。一图解千言万语,从如此精心设计的图便可快速而良好地了解Boost.Pool 的运作。要提醒你的是,Boost对于chunk和block两个术语的用法和其它系统的习惯都不相同,刚好相反,它称呼小区块为chunk,大区块为block。我无法权宜地在本文中调整称呼使与STL, Loki,MFC 的习惯相同,因为这些术语也反应在源码中,而我稍后要解释Boost 源码。所以请务必记住这一变化,切切。

Boost.Pool 和Loki 及MFC 一样维护不限个数的free lists ,也就是说它为各式各样的尺寸服务。它也和Loki 和MFC 一样对每个free list 都采取分段链接,只不过每个分段不似Loki

图6 : MFC CFixedAlloc

行为示意图

图7 : MFC 分配器的二个主要classes

的关系

C/C++ >>>

Programmer

97

技 术

或MFC 那样有固定的chunk (小区块)个数,而是每次向系统要求block (大区块)时便累加两倍chunks 数;一开始是32。这便是图8中的32,64,128小区块数的由来。因为chunks 个数会变,所以它不可能以Loki::Chunk 那种方式管理chunks ,而是采用类似MFC's CPlex 的实现,不过这次是以“尾部”来加持灌顶,这个尾部被设计为class PODptr (在命名空间boost::details 内,只供Boost 内部使用),不但必须像MFC's CPlex 那般维护一个指针指向下一分段,

而且必须记录下一分段的大小(bytes 数),因为每一分段大小不同嘛(大致两倍增长)。还请注意,Boost.Pool 刻意让每一分段以起始点实际地址之高低排列于free list ,如图8三个分段起始地址,007D09A4段的尾部指向007E0250段,而后者的尾部指向最后一段007E0490,这将有利于将来接受chunks 归还时比较容易实作。

管理free list 的是boost::pool 。它必须拥有一个PODptr (图8上方灰色部分)指向该free list 中的第一个blocks ,还需要记录chunk 大小和下次向系统要求的chunks 数(图8上方绿色部分),还要有一个指针指向目前可用的chunk (图8上方紫色部分)。稍后我们可以从源码中看到这些数据从何而来。

此前限于篇幅,我没有提过STL, Loki, MFC 的小区块归还动作(deallocation )。今天的主角是Boost ,我们可得好好说事。以图8为例,

该图表现的是已被客户要走32(第一个block 全部)+64(第二个block 全部)+1个chunks (图中灰色小区块),之后又归还两个chunks (白色小区块)的情况。归还的两个分别是中间block 的第一个chunk 和上方block 的第4个chunk 。由于各个分段(blocks )以地址在free list 中由低而高排列,因此可轻易计算出被归还的chunk 座落于哪一个block 内,从而顺利完成指针的移转和整个free list 的维护。当任何(或所有)blocks 中的所有chunks 都回收完毕,理论上这里记录有足够的信息可以把整个block (或所有blocks )释还给系统。但Boost 没那么做。原因呢?Boost 没说,我也不好说。也许你可以为它强化这一点,这是很好的练功题。Boost 唯一释还blocks 的时机,是在分配器离开作用域造成其析构函数被唤起时(这一点和MFC 很像),那有点时不我予,太晚了,没多大帮助。

图8 : Boost.Pool

的行为示意图

在这种设计架构下,Boost.Pool 的用法像这样:

Boost.Pool 的classes 结构和源码剖析

Boost.Pool 主要有6个.hpp 文件,位于 \Boost\include\boost-1_33\boost\pool 。各文件的主要内容见图9,最后两个文件本

文不讨论。

下期预告

Boost.Pool (下)要点 :

l 参数UserAllocator 决定实际用上的分配/释放工具

顾名思义UserAllocator 是一个可由用户指定的分配器,换句话说Boost 允许用户自行决定最底层的分配/释放动作采用哪一套基本工具。

l boost::simple_segregated_storage

这个class 的命名意义是“经过简易切割处理后的储存空间”。它维护一个指针first ,指向第一个可用小区块。将来用户要求分配小区块时,经过层层函数调用,只要这个first 不是null ,就直接拿first 所指的小区块来交差。

l boost::details::PODptr

PODptr 的命名表示,这是一个被当做指针的东西,用来指向一个POD 。什么是POD 呢?常常出现于C++文献的一个字眼,意思是“Plain Old Data ”,简朴的旧式数据型态,你也可以认为它是C++ class 以外的任何型式的数据组合,其大小就是其成员的单纯总和。C struct 就是一种POD 。

l 三阶段初始化

l 三阶段建构/分配(construct()/malloc())l 三阶段销毁/释放(destroy()/free())l 离开作用域时的清理动作

中的poolfwd.hpp 声明出所有相关classes ,详见图10。Boost 的classes 设计有点复杂,彼此有继承关

系(而且是罕见的protected 继承),这在前面所谈的各程序库中是没有的,稍后我有详细说明。

图10 :poolfwd.hpp 源码

刚才我们看过Boost.Pool 的用法,客户用的是最上层的boost::object_pool。这个class 本身不带任何成员变量。它继承boost::pool ,因此从对象模型的角度来说其object 相当于内含了一个boost::pool (亦即所谓的base class part )。这样的双层classes 切割只是为了将上下层函数功能做个区隔,没有其它意思(没有虚函数、没有多态),所以请注意,这里使用的继承关系是较为罕见的protected 继承而非常见的public 继承:

相对较低阶的boost::pool 负责管理free list 。先前说过它是利用一个个PODptr 个别管理一个个blocks (大区块)

许多大型C++程序库都有自己的内存管理机制,包括MFC, STL, Loki 都如此,Boost 自然也不例外。本文概要说明前三者的实现,然后集中探讨Boost 的实现与设计细节。

读者基础:

读者基础:拥有C++ templates 编程经验和C++标准库使用经验本文测试环境:VC6, VS2003

本系列文章将汇整至编写中的《Boost 运用与源码剖析》

本文关于STL, Loki, MFC, Boost 之Pooled Allocation 技术分析将汇整至编写中的《C++内存管理》

关键术语:Boost, Pool, Pooled Allocation, Embedded Pointers, chunks, blocks

?????

Boost.Pool

(下)

"Boost 技术与应用" 系列文章2

>>> C/C++

技 术

Technology

从源码摘录可看到boost::pool 的确内含一个PODptr (程序进行过程中随着blocks 的不敷使用,此类PODptr 还会陆续被创建出来)。boost::pool 也把若干基础工作切割给simple_segregated_storage ,并再次以protected 继承方式实现上下层关系:

前面所说的PODPtr 并不对外开放,被定义于boost 内层的namespace details 中。稍后我会详细说明其内的两个成员变量:

被boost::pool 切割出去的更基础工作由父类simple_segregated_storage 完成:

图11显示上述四个classes 的关系。图12是object_pool 的对象模型(内存布局),其中紫色、灰色、绿色分别由图11同色class 的成员变量贡献。稍后我要解释这四个classes 的主要行为并带领大家看源码。

有了以上大局观,现在我们可以任意讨论任何一层而不致迷失方向了。

参数UserAllocator 决定实际用上的分配/释放工具

UserAllocator 是一个template 参数,主要出现在下面这几个地方:

顾名思义UserAllocator 是一个可由用户指定的分配

器,换句话说Boost 允许用户自行决定最底层的分配/释放动作采用哪一套基本工具。先前例中并没有指定这个参数:

那是因为它有缺省值,定义于poolfwd.hpp ,见图10。甚至连boost::pool 也在其声明式中指定了自己的这个缺省参数值,亦见图10。

下面是可做为UserAllocator 用途的两个classes 。Boost 缺省使用第一个。

图12 boost::object_pool 的对象模型(内存布局)

C/C++ >>>

图11 Boost.Pool 的四个主要classes

的关系

两者都提供malloc()和free(),唯一差别在于其一内部用的是new[] 分配内存并以delete[] 释放,另一个以std::malloc() 分配并以std::free()释放。其实两者在常见的情况下效果相等。

boost::simple_segregated_storage

这个class 的命名意义是“经过简易切割处理后的储存空间”。它维护一个指针first ,指向第一个可用小区块。

它的构造函数将指针first 初始化为null 。

将来用户要求分配小区块时,经过层层函数调用,只要这个first 不是null ,就直接拿first 所指的小区块来交差。

如果first 指向null ,就表示这个“经过简易分割处理后的储存空间”已经耗尽(或从来没有过):

在“非空”前提下,小区块的分配是这么做的:

是的,在传回可用小区块的地址之前,当然要先调动first 指针,令它指向下一个可用小区块。但为什么竟是将它设为nextof(first)的返回值呢?这里有两件事需要讨论,第一,可用小区块是如何布署的?第二,nextof()做了什么?

先回答第一个问题:每一个可用小区块(chunk )都是在向系统要一个大区块(block )之后按给定尺寸切割出来的,采用嵌入式指针完成linked list ,像这样(这些动作在segregate()内完成,限于篇幅不列源码):

因此我们必须调整first 令它指向下一个可用小区块。也就是将first 的内容设为“目前first 所指物之前4 bytes 的内容”。问题是first 本身型别是void*,不能允许提领内容。如果先将first 转型为void**(只是转型,其值仍是原本的那个指针值),对它取值就会取到一个void*(4 bytes ),那正符合上图显示的布局。那就是nextof()的意义:

这个函数主要用来处理从first 指针一脉所指的指针。

boost::details::PODptr

PODptr 的命名表示,这是一个被当做指针的东西,用来指向一个POD 。什么是POD 呢?常常出现于C++文献的一个字眼,意思是“Plain Old Data ”,简朴的旧式数据型态,你也可以认为它是C++ class 以外的任何型式的数据组合,其大小就是其成员的单纯总和。C struct 就是一种POD 。

Boost.Pool 利用不对外公开的这个PODptr 来链接一个个blocks ,形成一个free list 。POD 代表的是“向系统索求一大块block 再加上必要的管理元素”,而所谓管理就是当block 内的每个chunks 都已被用掉,就链接另一个blocks 以便供应更多chunks 。PODPtr 结构如下:

? 4 czuft ? ?? ?

...

>>> C/C++

技 术

Technology

(以下数字皆为某种情况下的实例) :

然后是这样:

当第一个block 的所有chunks 统统被取光后,再变成这样:

当第二个block 的所有chunks 统统被用光后,又变成这样:

三阶段初始化

当用户这么写:

首先唤起构造函数:

更早一步又先唤起派生类boost::pool

的构造函数,并且T 是Foo ,UserAllocator 是缺省值default_user_allocator_new_delete :

这便设定了三个成员变量的初值:区块大小(nrequested_size )被设为sizeof(Foo),下次分配区块数(next_size )被设为32,“特形指针”PODptr 的sz 被设为0,ptr 被设为0。在此之前还会先唤起base class 构造函数:

设定指针first 为0。因此,初初创建一个为Foo 服务的object_pool 获得的结果是:

三阶段建构/分配(construct()/malloc()

欲从上述的fooPool 分配区块,可调用construct()或malloc()。前者会调用后者并于分配点建构一个元素,后者只是单纯分配内存。先看前者:

construct()另有许多版本,全都定义于上述最后出现的两个.inc 之一(上述最后的条件编译用来判断编译器是否允许template 参数拥有const 和volatile ,即所谓CV

modifiler )。如果用户这么写:

C/C++ >>>

于是我们追踪上述第一行的boost::object_pool::malloc():

这里最后一行调用了一个奇怪的函数store()。那是什么东东?

在Boost.Pool 的三层继承体系中,由于存在许多签名式(signature )相同的non-virtual 函数,例如

boost::object_pool::malloc();boost::pool::malloc();

boost::simple_segregated_storage::malloc();以及

boost::pool::free(void* const chunk);

boost::simple_segregated_storage::free(void* const chunk);以及

boost::pool::order_free(void* const chunk);b o o s t ::s i m p l e _s e g r e g a t e d _s t o r a g e ::

ordered_free(void* const chunk);

以及

boost::pool::nextof(void* const ptr);

boost::simple_segregated_storage::nextof(void* const ptr);

这些上下层提供的签名式相同的non-virtual 函数彼此遮蔽(hide ),因此derived class 若欲调用其base class 中的这类函数,办法之一是先将自己转型为base class (这是 up-cast ,一定安全)。而store()就做这件事情:

???

??

??

??

这些函数都是将自己('this' object )传出去,并强制改头换面成为另一种type 。由于以by reference 方式传出,传的是本身而不是复件。改头换面之后可被拿来调用新身份之任何函数而不受遮蔽(hide )之苦了。 回到先前讨论的:

我们已知store()的意义,因此最后一行唤起boost::pool::ordered_malloc():

其中再次使用store()做向上转型动作(如先前说明),然后判断是否为空:

显然是要看看first 指针是否所指有物。我们知道一开始first 指针为0,所以判断之后唤起的函数应该是pool 的ordered_malloc_need_resize()。这个函数可就复杂些了,作用相当于分配一大区块、切割为若干小区块并链接为一个linked list 。源码分析太占篇幅,不适合在杂志上呈现。总之,获得的成果如下:

尔后用户如果再调用31次fooPool.construct()或fooPool.malloc(),情况如下,注意所有32个chunks 都被取用了,first 指针又变成null :

fooPool

>>> C/C++

技 术

Technology

当用户再次调用fooPool.construct()或fooPool.malloc(),情况如下(又一次分配、细切、链接整合、给出一个chunk ):

三阶段销毁/释放(destroy()/free())

当客户欲归还来自fooPool 的chunks ,可调用destroy()或free()。前者会先调用chunk 地址上的那个object 的析构函数然后调用后者:

继续调用下去:

再继续调用下去:

其中调用的find_prev()我就不列源码了,只以图片说

明情况的变化。

假设目前有一个block 已分配5 chunks 出去,指针first 指向第6个chunk :

归还p 2指针时,结果如下:

归还p 4指针时,结果如下:

换个例子。假设目前有3个blocks 如下(注意free lists 的每个blocks 都按其实际地址高低链接起来,由低而高,例如图中blocks 的起始点先是007D 09A 4然后是007E 0250再然后是007E 0490):

归还p 4,造成:

再归还pp 1,造成:

fooPool

C/C++ >>>

技 术

Technology

Array 和Any

“Boost 技术与应用”系列文章3

C/C++ >>>

图1 / boost::array 关键源码(摘自 \boost\include\boost-1_33\

boost\array.hpp )

撇开若干新增函数,这一版与前三个版本的最大差异在于它以索引存取元素时(也就是at() 和operator[])做了范围检查。检查方式一是使用rangecheck()在逾越范围时丢出range_error 异常,另一是使用BOOST_ASSERT 宏,一般情况下等同于std::assert (注意,调试模式下才有作用)。图2是一份测试码,其内有充分注释。虽然我没有讲解Boost.Array 的每一成份,但这里所列的源码剖析和测试实例,对程序员关心的绝大多数场合来说,应该是足够了。

图2 / 测试Boost.Array

◎ 文/侯捷

本文主要讨论Boost 的双A :Array 和Any 。前者是Boost 对语言构件array 的改进,

后者是一种可表现任何types 的type

>>> C/C++

相关主题
文本预览
相关文档 最新文档