《Small Memory Software:Patterns For System With Limited Memory》读书笔记

原文地址:http://blog.csdn.net/jinzhuojun/article/details/13297447程序员

 

虽然摩尔定律让咱们的计算机硬件得以以指数速度升级,但反摩尔定律又不断消减这些升级所带来的好处。其缘由之一就是面对硬件的更新换代,程序员彷佛不用再对内存精打细处“了。而近年来随着穿戴式设备和大数据平台的兴起(一个是内存自己受限,一个是对内存的需求巨大),让内存的有效利用又成为了值得开发人员关注的热点。算法

 

Small MemorySoftware: Patterns For System With Limited Memory 》(http://www.smallmemory.com/ , 中文名为《内存受限系统之软件开发:针对内存受限系统而整理的模式》)一书探讨了编程中有效利用内存的方法。全书分为五个部分,分别从架构,次级存储,压缩,数据结构和内存分配进行了阐述。下面是一些读书笔记,留做备忘。数据库

 

Small Architecture(小内存架构)编程

Memory LimitFixed-sized Heap,Memory Partitions):让系统中的每一个组件负责本身的内存,为每一个组件设置配额,一旦用完即分配失败。这种模式主要是为了防止系统中某一组件耗光系统全部内存的状况发生,而缺点是容易引发内存浪费,由于会出现系统中一些组件还有空闲内存而另一些内存分配失败的状况。实现上主要有三种方法:截获内存申请释放操做,独立堆和独立进程。浏览器

Small Interface当整个系统被分红若干个组件,而每一个组件又独立管理本身的内存使用,那么当内存数据在这些组件间进行传递时,就须要定义接口(或者说协议)。在组件间交换数据有三种方式:Lending(客户传对象给系统组件), Borrowing(系统组件传对象给客户)和Stealing(可双向,同时对象全部权也发生变化)。另外组件间的数据传递也常常会经过Iterator这种增量式的形式来进行。缓存

Partial FailureGraceful degradation,Feast and Famine):当系统申请内存失败时,让系统在一个“降级模式”下继续运行而不是直接退出。如字体加载失败就用系统字体,图片加载失败就用占位符代替。那些由于内存不足而没法进行的计算能够不执行或是缓存起来之后执行。当内存不那么紧张时,系统要能恢复到先前的模式。值得注意的是,处理Partial Failure自己也须要内存,所以须要预分配这部份内存(称为Rainy day fund)。后面的Multiple representationApplicationSwitching均可做为其实现形式。数据结构

Captain OatesCache Release):当整个系统内存紧张时,砍掉没必要要或是次要的组件,亦或是清空一些缓存得以让整个系统继续运行。它的核心思想是牺牲那些次要的功能来留全那些主要的功能。Captain OatesPartial Failure中不少技术是通用的,前者描述其它组件内存不足时本组件该干什么,后者描述当前组件内存不足时本组件该干什么。架构

Read-Only MemoryUse the ROM):有些数据是不常更新的,如可执行代码,常量字符串,资源文件等。把这些数据放在只读存储区域中,至少有两个好处:其一是能够经过共享节约内存;另外一好处是要换出到次级存储时不用写回操做,从而提升了效率。若是须要更新这些只读数据能够用后面会提到的Copy-on-write或者Hooksapp

HooksVector table, Jumptable, Patch table, Interrupt table):想要修改只读存储区域上的数据,能够经过间接访问只读内存的方法,中间用位于可写区域的Hook作跳板。Hook的一个例子就是GOT表,代码段被加载在只读段中,而GOT表加载在可写段中,由于它是要在运行时被Dynamic linker修改的。这样当代码段中有外部函数调用时,会经过GOT表来跳转。这样就实现了代码段不变而改变程序逻辑的目的。函数

 

Secondary Storage (次级存储)

Application SwitchingPhases, ProgramChaining, Command Scripts):假设系统提供不少功能而用户不会同时用到,就能够把系统分为不少个独立的可执行程序,等用户须要哪一个再起哪一个,每次只运行一个。这相似于Unix命令的哲学,把一系列功能专注的独立工具组合链接起来完成强大的功能。程序之间可经过参数,磁盘和环境变量等转递信息。要传输复杂的对象还可能会用到Memento模式。

Data File PatternBatch Processing,Filter, Temporary File):若是要处理的数据太多,无法或是不必一次所有载入,就一批批从次级存储放进内存处理。例如要将一个没法一次放入内存的大数据排序,能够先将之切分使之可以载入内存,而后分别进行归并排序,直到全部数据排序完成。

Resource File Pattern资源文件通常包括字体,图标,字符串,布局和配置信息等。这些东西通常不会被改写,且在程序运行的任什么时候候都有可能用到,但用完后便可以被丢到次级存储器上,下次要用能够再去磁盘上取。

PackagesComponents, LazyLoading, Dynamic Loading, Code Segmentation):将整个系统分红不少功能包,要什么功能的时候再动态加载。加载能够以单独进程形式也能够动态连接,亦或手工加载进内存,它们各有利弊。如浏览器插件,Java中的classLinux中的driver都是利用了这种动态加载的概念。实际要注意的是binary级的匹配问题。Abstract Factory模式可用来统一客户和包实现的接口。Proxy模式可用于包的自动加载和卸载。

Paging PatternVirtual Memory,Persistence, Backing Store, Paging OO DBMS):基于空间局部性,将程序最近经常使用的数据(Working set)保留在内存中,其他放在次级存储器中,要用时再读进来。它在形式上按粒度从小到大有Demanding page(页级), Object Oriented Databases(对象级)和Swapping(进程数据级)。典型的例子固然就是操做系统中的页式管理了。

 

Compression(压缩)

Table CompressionPatternNibble Coding, HuffmanCoding):因为数据中每一个元素出现几率不同(信息量不同)或者说重要性程度不同,所以若是对原始数据进行从新编码,能够达到压缩的目的。典型的例子如Huffman编码(基于字符出现频率不同)和JPEG(基于人眼对色彩的敏感程度不同)。

Difference CodingPatternDelta Coding, RunLength Encoding):差分编码,即利用数据(尤为是音、视频等流数据)的先后相关性进行压缩编码,如MPEG格式。咱们知道压缩视频中通常有关键帧和非关键帧之分,关键帧可被独立解码,而非关键帧只存有差分信息。差分编码的实现技术有Delta CodingRun Length EncodingLossyDifference Compression,此外还需为传错而考虑重同步问题。

Adaptive CompressionPattern在压缩前或者压缩中分析数据来获得最好的压缩参数或实时调整参数。与前两种方式相比自适应算法处理时间更长,所以不适用于实时任务。如gzip, bzip2压缩方法就使用了该模式。

 

Small Data Structure(小内存数据结构)

Packed DataBit Packing一方面咱们有时会用过大的类型存储数据,另外一方面计算机体系通常会将数据进行对齐存储从而优化读写效率,有时这会致使内存利用效率的下降。咱们能够经过将数据结构进行pack来减小内存的使用。缺点是它会较大地影响性能。注意将用句柄代替指针也能节省一部份内存,由于句柄通常都比指针小,能够用更小的数据类型存储。

Sharing Normalisation):内存共享,节约内存。共享的东西在内存中仅须要一份拷贝。有些是明显能够共享的,就像动态库libc.so,有些因为内容重复能够共享的,如Java中的String使用了Flyweight模式,对重复字符串只存储一份。简单的共享能够用静态全局变量,Sigleton模式或者LazyInitialization实现。复杂点的能够用Shared cache,即把全部的共享对象放入数据库(cache),使用时用key查询来获得。

Copy-on-Write:共享的东西须要改动时,就将之拷贝一份,再进行读写。像Linux中对于共享页在页表中是标识成只读的,当对其进行写操做时,就会发生page fault,从而把控制权交还kernel进行Copy-on-write的处理。Copy-on-write经常使用Proxy模式实现,以实现过程对客户透明。

Embedded Pointer对于一些用指针组建的数据结构(如链表,树,图等),将先后指针放在要存储的目标对象中,不只能节省维护数据结构自己的内存,并且能在遍历时节省临时内存(如栈,队列)的使用。实现起来有三种方式:继承,内联和预处理。像Linux中的list_head就是用了内联嵌入指针。另外相关的技术还有Pointer DifferencesPointer reversal

Multiple RepresentationTemplate MethodStrategy模式的做用差很少,只是这儿用在内存利用上了。即准备多套实现,根据内存的使用状况进行切换。该模式一大优势是对客户透明。例子如JavaSTL中的容器类,它们接口统一,但对内存的使用策略却不一样。

 

Memory Allocation(内存分配)

原则上只要能知足需求,能用简单的就用简单的。内存分配主要就是和两个问题做斗争:一是碎片问题(解决方法有固定分配,拷贝压缩,池式管理等);二是内存不足问题(解决方法有固定客户内存大小,产生错误信号,下降质量,删除旧对象,延迟申请和忽略问题等)。

Fixed AllocationStatic Allocation,Pre-allocation):初始化时分配固定大小内存,程序执行时再也不动态分配,而初始化时能够动态也能够静态分配。这样作的好处是不会出现分配失败的状况,系统可预测性好,且内存分配高效,坏处是不够灵活。

Variable AllocationDynamic Allocation):当须要的时候再分配内存,适合预先不知道数据大小的状况(如通用库)。它的缺点也是Fixed Allocation的优势:即分配可能失败(此时能够考虑引入Partial Failure),分配操做会影响性能(将Fixed Allocation与之结合,就有了Pooled Allocation),须要本身释放内存(可用ReferencecountingGarbage Collection),另外动态分配还更容易产生碎片(可用Compaction压缩已有对象,MemoryDiscard删除临时对象)。

Memory DiscardStack Allocation,Scratchpad):适用于临时用的内存。在操做前从栈、堆或是预分配区域中进行分配,操做完了就一块儿丢弃。好处是内存的分配和释放都只须要移动下指针便可,简单粗暴高效。函数调用过程当中栈帧的处理就是该模式的例子。另外一种少见的状况就是要用一个现成的但有内存泄露问题的组件,能够在加载它以前为之分配临时内存,卸载时把它的内存所有统一清空。用Memory Discard的时候要格外当心临时对象所持有的外部资源引用,在释放内存前要先释放这些资源。

Pooled AllocationMemory Pool:内存池适用于大小相似,但次数较为频繁的内存分配请求。一般作法是先预分配一块内存(经过Fixed Allocation),将其按固定大小切分红为内存池,用空闲队列进行管理。程序执行过程当中须要内存了就从里边拿,释放后也放回其中(Variable Allocation)。该模式结合了Fixed AllocationVariableAllocation的优势。Linux中的Slab就是个例子。

CompactionManged Table, MemoryHandles, Defragmentation):压缩主要用来处理Variable Allocation产生的碎片问题。当系统跑了很长一段时间后,剩下不少不连续的空闲内存,当用户申请一块大的内存时,申请就容易失败。比较简单的方法就是拷贝数据让它们整一起去。注意因为指针的存在,这些指针在压缩过程当中须要被及时更新,该问题通常可用间接引用(即用句柄或对象表代替指针)来解决。

Reference Counting主要用于共享对象的回收,使这些对象在没人使用时被自动回收。大致思路是在对象中记录该对象的引用数来判断其是不是垃圾,当引用数为0时说明无人引用,能够被清除。和GarbageCollection相比,它的优势是自己的overhead被分摊到整个执行过程,所以对用户的实时响应影响不大,且一旦对象变垃圾当即会被清除;缺点是须要程序员来主动维护引用计数,且影响系统性能,另外要处理环形引用比较麻烦。

Garbage CollectionMark-sweep GarbageCollection, Tracing Garbage Collection):也是主要用来处理共享对象的回收。系统在必定时候(通常是现有内存不足时)触发垃圾回收,而后从一些根结点(一般为栈变量,全局及静态变量,外部库的引用等)出发遍历全部被引用对象。这样剩下的就是能够被清除的垃圾了。清除垃圾有两种作法:Mark-sweep GCCopying GC,实际中它们可结合使用(像mono中的SGen)。Garbage Collection的优势是更加自动,无需用户参于,且整体性能损失相对更小;缺点是回收时会有停顿现象,另外对Finalization(通常用于释放文件句柄或设备等外部资源)的支持会比较麻烦(通常作法是把它们加入到待办队列,而后到单独线程去调用,但这样它们被调用的时间就很差预测,容易使系统变得不稳定)。因为该模式和Reference Counting各有利弊,有些时候系统中它们会同时出现。

相关文章
相关标签/搜索