C++应用程序性能优化(书)内存管理

本质上虚拟内存就是要让一个程序的代码和数据在没有全部载入内存时即可运行。运行过程中,当执行到尚未载入内存的代码,或者要访问还没有载入到内存的数据时,虚拟内存管理器动态地将这部分代码或数据从硬盘载入到内存中。而且在通常情况下,虚拟内存管理器也会相应地先将内存中某些代码或者数据置换到硬盘中,为即将载入的代码或数据腾出空间。

因为内存和硬盘之间的数据传输相对代码执行来说,是非常慢的操作,因此虚拟内存管理器在保证工作正确的前提下,还必须考虑效率因素。比如,它需要优化置换算法,尽量避免就要执行的代码或访问的数据刚被置换出内存,而很久没有访问的代码或数据却一直驻留在内存中。另外它还需要将驻留在内存的各个进程的代码或数据维持在一个合理的数量上,并且根据该进程的性能表现动态调整此数量,等等,使得程序运行时将其涉及的磁盘I/O次数降到尽可能低,以提高程序的运行性能。

本章前一部分着重介绍Windows的虚拟内存管理机制,后一部分则简要介绍Linux的虚拟内存管理机制。

4.1  Windows内存管理

如果从应用程序的角度来看Windows虚拟内存管理系统,可以扼要地归结为一句话。即Win32虚拟内存管理器为每一个Win32进程提供了进程私有且基于页的4 GB(32位)大小的线性虚拟地址空间,这句话可以分解如下:

(1)“进程私有”意味着每个进程都只能访问属于自己的地址空间,而无法访问其他进程的地址空间,也不用担心自己的地址空间会被其他进程看到(父子进程例外,比如调试器利用父子进程关系来访问被调试进程的地址空间,这里不详述)。需要注意的是,进程运行时用到的dll并没有属于自己的虚拟地址空间。而是其所属进程的虚拟地址空间,dll的全局数据,以及通过dll函数申请的内存都是从调用其进程的虚拟地址空间中开辟。

(2)“基于页”是指虚拟地址空间被划分为多个称为“页”的单元,页的大小由底层处理器决定,x86中页的大小为4 KB。页是Win32虚拟内存管理器处理的最小单元,相应的物理内存也被划分为多个页。虚拟内存地址空间的申请和释放,以及内存和磁盘的数据传输或置换都是以页为最小单位进行的。

(3)“4 GB大小”意味着进程中的地址取值范围可以从0x00000000到0xFFFFFFFF。Win32将低区的2 GB留给进程使用,高区的2 GB则留给系统使用。

Win32中用来辅助实现虚拟内存的硬盘文件称为“调页文件”,可以有16个,调页文件用来存放被虚拟内存管理器置换出内存的数据。当这些数据再次被进程访问时,虚拟内存管理器会先将它们从调页文件中置换进内存,这样进程可以正确访问这些数据。用户可以自己配置调页文件。出于空间利用效率和性能的考虑,程序代码(包括exe和dll文件)不会被修改,所以当它们所在的页被置换出内存时,并不会被写进调页文件中,而是直接抛弃。当再次被需要时,虚拟内存管理器直接从存放它们的exe或dll文件中找到它们并调入内存。另外对exe和dll文件中包含的只读数据的处理与此类似,也不会为它们在调页文件中开辟空间。

当进程执行某段代码或者访问某些数据,而这些代码或者数据还没有在内存时,这种情形称为“缺页错误”。缺页错误的原因有很多种,最常见的一种就是已经提到的,即这些代码和数据被虚拟内存管理器置换出了内存,这时虚拟内存管理器在这段代码执行或者这些数据被访问前将它们调入内存。这个操作对开发人员来说是透明的,因此大大简化了开发人员的负担。但是调页错误涉及磁盘I/O,大量的调页错误会大大降低程序的总体性能。因此需要了解缺页错误的主要原因,以及规避它们的方法。

4.1.1  使用虚拟内存

Win32中分配内存分为两个步骤:“预留”和“提交”。因此在进程虚拟地址空间中的页有3种状态:自由(free)、预留(reserved)和提交(committed)。

(1)自由表示此页尚未被分配,可以用来满足新的内存分配请求。

(2)预留指从虚拟地址空间中划出一块区域(region,页的整数倍数大小),划出之后这个区域中的页不能用来满足新的内存分配请求,而是用来供要求“预留”此段区域的代码以后使用。预留时并没有分配物理存储,只是增加了一个描述进程虚拟地址空间使用状态的数据结构(VAD,虚拟地址描述符),用来记录这段区域已被预留。“预留”操作相对较快,因为没有真正分配物理存储。也正因为没有分配真正的物理存储,所以预留的空间并不能够直接访问,对预留页的访问会引起“内存访问违例”(内存访问违例会导致整个进程立刻退出,而不仅仅是中止引起该违例的线程)。

(3)提交,若想得到真正的物理存储,必须对预留的内存进行提交。提交会从调页文件中开辟空间,并修改VAD中的相应项。注意,提交时也并没有立刻从物理内存中分配空间,而只是从磁盘的调页文件中开辟空间。这个空间用做以后置换的备份空间,直到有代码第一次访问这段提交内存中的某些数据时,系统发现并没有真正的物理内存,抛出缺页错误。虚拟内存管理器处理此缺页错误,直到这时才会真正分配物理内存,提交也可以在预留的同时一起进行。需要注意的是,提交操作会从调页文件中开辟磁盘空间,所以比预留操作的时间长。

这也是Win32虚拟内存管理中的demand-paging策略的一个体现,即不到真正访问时,不会为某虚拟地址分配真正的物理内存。这种策略一是出于性能考虑,将工作分段完成,提高总体性能;二是出于空间效率考虑,不到真正访问时,Win32总是假定进程不会访问大多数的数据,因而也不必为它们开辟存储空间或将其置换进物理内存,这样可以提高存储空间(磁盘和物理内存)的使用效率。

设想某些程序对内存有很大的需求,但又不是立即需要所有这些内存,那么一次就从物理存储中开辟空间满足这些还只是“潜在”的需求,从执行性能和存储空间效率来说,都是一种浪费。因为只是“潜在”需求,极有可能这些分配的内存中很大一部分最后都没有真正被用到。如果在申请的时候就一次性为它们分配全部物理存储,无疑会极大地降低空间的利用效率。

另一方面,如果完全不用预留及提交机制,只是随需分配内存来满足每次的请求,那么对一个会在不同时间点频繁请求内存的代码来说,因为在它请求内存的不同时间点的间隙极有可能会有其他代码请求内存。这样这段在不同时间点频繁请求内存的代码请求得到的内存因为虚拟地址不连续,无法很好地利用空间locality特性,对其整体进行访问(比如遍历操作)时就会增加缺页错误的数量,从而降低程序的性能。

预留和提交在Win32中都使用VirtualAlloc函数完成,预留传入MEM_RESERVE参数,提交传入MEM_COMMIT参数。释放虚拟内存使用VirtualFree函数,此函数根据不同的传入参数,与VirtualAlloc相对应,可以释放与虚拟地址区域相对应的物理存储,但该虚拟地址区域还可处于预留状态,也可以连同虚拟地址区域一起释放,该段区域恢复为自由状态。

线程栈和进程堆的实现都利用了这种预留和提交两步机制,下面仅以线程栈为例来说明Win32系统是如何使用这种预留和提交两步机制的。

创建线程栈时,只是一个预留的虚拟地址区域,默认是1 MB(此大小可在CreateThread或在链接时通过链接选项修改),初始时只有前两页是提交的。当线程栈因为函数的嵌套调用需要更多的提交页时,虚拟内存管理器会动态地提交该虚拟地址区域中的后续页以满足其需求,直到到达1 MB的上限。当到达此预留区域大小的上限(默认1 MB)时,虚拟内存管理器不会增加预留区域大小,而是在提交最后一页时抛出一个栈溢出异常,抛出栈溢出异常时该栈还有一页空间可用,程序仍可正常运行。而当程序继续使用栈空间,用完最后一页后,还继续需要存储空间,这时就超过了上限,会直接导致进程退出

所以为防止线程栈溢出导致整个程序退出,应该注意尽量控制栈的使用大小。比如减少函数的嵌套层数,减少递归函数的使用,尽量不要在函数中使用太大的局部变量(大的对象可以从堆中开辟空间存放,因为堆会动态扩大,而线程栈的可用内存区域在线程创建时就已固定,之后在整个线程生命期间无法扩展)。

另外为了防止因为一个线程栈的溢出导致整个进程退出,可以对可能会产生线程栈溢出的线程体函数加异常处理,捕获在提交最后一页时抛出的溢出异常,并做出相应处理。

4.1.2  访问虚拟内存时的处理流程

对某虚拟内存区域进行了预留并提交之后,就可以对该区域中的数据进行访问了,下图描述了当程序对某段内存访问时的处理流程:

如图4-1所示,当该数据已在物理内存中时,虚拟内存管理器只需将指向该数据的虚拟地址映射为物理指针,即可访问到物理内存中的真正数据。这一步不会涉及磁盘I/O,速度相对较快。

当第一次访问一段刚刚提交的内存中的数据时,因为并没有真正的物理内存分配给它。或者该数据以前已被访问过,但是被虚拟内存管理器置换出了内存。这两种情形都会引发缺页错误,虚拟内存管理器此时会处理这一缺页错误,它先检测此数据是否在调页文件中已有备份空间(exe和dll的代码页和只读数据页情形与此类似,但是其备份空间不在调页文件,而是包含它们的exe或dll文件)。如果是这两种情况,表明访问的数据在磁盘中有备份,接下来虚拟内存管理器就需要在物理内存中找到合适的页,并将存放在磁盘的备份数据置换进物理内存。

图4-1  访问虚拟内存的处理流程

虚拟内存管理器首先查询当前物理内存中是否有空闲页,虚拟内存管理器维护一个称为“页帧数据库”(page-frame database)的数据结构,此数据结构是操作系统全局的,当Windows启动时被初始化,用来跟踪和记录物理内存中每一个页的状态,它会用一个链表将所有空闲页连接起来,当需要空闲页时,直接查找此空闲页链表,如果有,直接使用某个空闲页;否则根据调页算法首先选出某个页。需要指出的是,虚拟内存管理器调页时并不是只调入一个页,为了利用局部特性,它在调入包含所需数据的页的同时,会将其附近的几个页一起调入内存。这里为了简单和清楚起见,假定只调入目标页。但应该意识到Win32调页时的这个特性,因为可以利用它来提高程序效率。这个页将会用来存放即将从磁盘置换进来的页的内

C++性能优化 指南(强列推荐) chm版 Part I: Everything But the Code Chapter 1. Optimizing: What Is It All About? Performance Footprint Summary Chapter 2. Creating a New System System Requirements System Design Issues The Development Process Data Processing Methods Summary Chapter 3. Modifying an Existing System Identifying What to Modify Beginning Your Optimization Analyzing Target Areas Performing the Optimizations Summary Part II: Getting Our Hands Dirty Chapter 4. Tools and Languages Tools You Cannot Do Without Optimizing with Help from the Compiler The Language for the Job Summary Chapter 5. Measuring Time and Complexity The Marriage of Theory and Practice System Influences Summary Chapter 6. The Standard C/C++ Variables Variable Base Types Grouping Base Types Summary Chapter 7. Basic Programming Statements Selectors Loops Summary Chapter 8. Functions Invoking Functions Passing Data to Functions Early Returns Functions as Class Methods Summary Chapter 9. Efficient Memory Management Memory Fragmentation Memory Management Resizable Data Structures Summary Chapter 10. Blocks of Data Comparing Blocks of Data The Theory of Sorting Data Sorting Techniques Summary Chapter 11. Storage Structures Arrays Linked Lists Hash Tables Binary Trees Red/Black Trees Summary Chapter 12. Optimizing IO Efficient Screen Output Efficient Binary File IO Efficient Text File IO Summary Chapter 13. Optimizing Your Code Further Arithmetic Operations Operating System–Based Optimizations Summary Part III: Tips and Pitfalls Chapter 14. Tips Tricks Preparing for the Future Chapter 15. Pitfalls Algorithmic Pitfalls Typos that Compile Other Pitfalls
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值