堆和栈的区别.doc

上传人:hw****26 文档编号:4200497 上传时间:2019-10-03 格式:DOC 页数:30 大小:149.50KB
下载 相关 举报
堆和栈的区别.doc_第1页
第1页 / 共30页
堆和栈的区别.doc_第2页
第2页 / 共30页
堆和栈的区别.doc_第3页
第3页 / 共30页
堆和栈的区别.doc_第4页
第4页 / 共30页
堆和栈的区别.doc_第5页
第5页 / 共30页
点击查看更多>>
资源描述

1、堆和栈的区别 一、预备知识程序的内存分配 一个由 c/C+编译的程序占用的内存分为以下几个部分 1、栈区(stack) 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其 操作方式类似于数据结构中的栈。 2、堆区(heap) 一般由程序员分配释放, 若程序员不释放,程序结束时可能由 OS 回 收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表,呵呵。 3、全局区(静态区)(static),全局变量和静态变量的存储是放在一块的,初始化的 全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另 一块区域。 - 程序结束后有系统释放 4、文字常量区常量字

2、符串就是放在这里的。 程序结束后由系统释放 5、程序代码区存放函数体的二进制代码。 二、例子程序 这是一个前辈写的,非常详细 /main.cpp int a = 0; 全局初始化区 char *p1; 全局未初始化区 main() int b; 栈 char s = “abc“; 栈 char *p2; 栈 char *p3 = “123456“; 1234560 在常量区,p3 在栈上。 static int c =0; 全局(静态)初始化区 p1 = (char *)malloc(10); p2 = (char *)malloc(20); 分配得来得 10 和 20 字节的区域就在堆区。

3、strcpy(p1, “123456“); 1234560 放在常量区,编译器可能会将它与 p3 所指向的 “123456“优化成一个地方。 二、堆和栈的理论知识 2.1 申请方式 stack: 由系统自动分配。 例如,声明在函数中一个局部变量 int b; 系统自动在栈中为 b 开辟空 间 heap: 需要程序员自己申请,并指明大小,在 c 中 malloc 函数 如 p1 = (char *)malloc(10); 在 C+中用 new 运算符 如 p2 = (char *)malloc(10); 但是注意 p1、p2 本身是在栈中的。 2.2 申请后系统的响应 栈:只要栈的剩余空间大于所

4、申请空间,系统将为程序提供内存,否则将报异常提示栈溢 出。 堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时, 会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表 中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的 首地址处记录本次分配的大小,这样,代码中的 delete 语句才能正确的释放本内存空间。 另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部 分重新放入空闲链表中。 2.3 申请大小的限制 栈:在 Windows 下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这

5、句话的意 思是栈顶的地址和栈的最大容量是系统预先规定好的,在 WINDOWS 下,栈的大小是 2M(也 有的说是 1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时, 将提示 overflow。因此,能从栈获得的空间较小。 堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储 的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小 受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。 2.4 申请效率的比较: 栈由系统自动分配,速度较快。但程序员是无法控制的。 堆是由 new 分配的内存,一般速度比较

6、慢,而且容易产生内存碎片,不过用起来最方便. 另外,在 WINDOWS 下,最好的方式是用 VirtualAlloc 分配内存,他不是在堆,也不是在栈 是直接在进程的地址空间中保留一快内存,虽然用起来最不方便。但是速度快,也最灵活。 2.5 堆和栈中的存储内容 栈: 在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可 执行语句)的地址,然后是函数的各个参数,在大多数的 C 编译器中,参数是由右往左入 栈的,然后是函数中的局部变量。注意静态变量是不入栈的。 当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地 址,也就是主函数中的下一条指令,程序

7、由该点继续运行。 堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容有程序员安排。 2.6 存取效率的比较 char s1 = “aaaaaaaaaaaaaaa“; char *s2 = “bbbbbbbbbbbbbbbbb“; aaaaaaaaaaa 是在运行时刻赋值的; 而 bbbbbbbbbbb 是在编译时就确定的; 但是,在以后的存取中,在栈上的数组比指针所指向的字符串(例如堆)快。 比如: #include void main() char a = 1; char c = “1234567890“; char *p =“1234567890“; a = c1; a = p1;

8、 return; 对应的汇编代码 10: a = c1; 00401067 8A 4D F1 mov cl,byte ptr ebp-0Fh 0040106A 88 4D FC mov byte ptr ebp-4,cl 11: a = p1; 0040106D 8B 55 EC mov edx,dword ptr ebp-14h 00401070 8A 42 01 mov al,byte ptr edx+1 00401073 88 45 FC mov byte ptr ebp-4,al 第一种在读取时直接就把字符串中的元素读到寄存器 cl 中,而第二种则要先把指针值读到 edx 中,在根据

9、edx 读取字符,显然慢了。 2.7 小结: 堆和栈的区别可以用如下的比喻来看出: 使用栈就象我们去饭馆里吃饭,只管点菜(发出申请)、付钱、和吃(使用),吃饱了就 走,不必理会切菜、洗菜等准备工作和洗碗、刷锅等扫尾工作,他的好处是快捷,但是自 由度小。 使用堆就象是自己动手做喜欢吃的菜肴,比较麻烦,但是比较符合自己的口味,而且自由 度大。 windows 进程中的内存结构 在阅读本文之前,如果你连堆栈是什么多不知道的话,请先阅读文章后面的基础知识。 接触过编程的人都知道,高级语言都能通过变量名来访问内存中的数据。那么这些变量在 内存中是如何存放的呢?程序又是如何使用这些变量的呢?下面就会对此进

10、行深入的讨论。 下文中的 C 语言代码如没有特别声明,默认都使用 VC 编译的 release 版。 首先,来了解一下 C 语言的变量是如何在内存分部的。C 语言有全局变量(Global)、本地 变量(Local),静态变量(Static)、寄存器变量(Regeister)。每种变量都有不同的分配方 式。先来看下面这段代码: #include int g1=0, g2=0, g3=0; int main() static int s1=0, s2=0, s3=0; int v1=0, v2=0, v3=0; /打印出各个变量的内存地址 printf(“0x%08xn“, /打印各本地变量的内存

11、地址 printf(“0x%08xn“, printf(“0x%08xnn“, printf(“0x%08xn“, /打印各全局变量的内存地址 printf(“0x%08xn“, printf(“0x%08xnn“, printf(“0x%08xn“, /打印各静态变量的内存地址 printf(“0x%08xn“, printf(“0x%08xnn“, return 0; 编译后的执行结果是: 0x0012ff78 0x0012ff7c 0x0012ff80 0x004068d0 0x004068d4 0x004068d8 0x004068dc 0x004068e0 0x004068e4 输出

12、的结果就是变量的内存地址。其中 v1,v2,v3 是本地变量,g1,g2,g3 是全局变量, s1,s2,s3 是静态变量。你可以看到这些变量在内存是连续分布的,但是本地变量和全局变 量分配的内存地址差了十万八千里,而全局变量和静态变量分配的内存是连续的。这是因 为本地变量和全局/静态变量是分配在不同类型的内存区域中的结果。对于一个进程的内存 空间而言,可以在逻辑上分成 3 个部份:代码区,静态数据区和动态数据区。动态数据区 一般就是“堆栈”。“栈(stack)”和“堆(heap)”是两种不同的动态数据区,栈是一种线 性结构,堆是一种链式结构。进程的每个线程都有私有的“栈”,所以每个线程虽然代

13、码 一样,但本地变量的数据都是互不干扰。一个堆栈可以通过“基地址”和“栈顶”地址来 描述。全局变量和静态变量分配在静态数据区,本地变量分配在动态数据区,即堆栈中。 程序通过堆栈的基地址和偏移量来访问本地变量。 低端内存区域 动态数据区 代码区 静态数据区 高端内存区域 堆栈是一个先进后出的数据结构,栈顶地址总是小于等于栈的基地址。我们可以先了解一 下函数调用的过程,以便对堆栈在程序中的作用有更深入的了解。不同的语言有不同的函 数调用规定,这些因素有参数的压入规则和堆栈的平衡。windows API 的调用规则和 ANSI C 的函数调用规则是不一样的,前者由被调函数调整堆栈,后者由调用者调整堆

14、栈。 两者通过“_stdcall”和“_cdecl”前缀区分。先看下面这段代码: #include void _stdcall func(int param1,int param2,int param3) int var1=param1; int var2=param2; int var3=param3; printf(“0x%08xn“,m1); /打印出各个变量的内存地址 printf(“0x%08xn“,m2); printf(“0x%08xnn“,m3); printf(“0x%08xn“, printf(“0x%08xn“, printf(“0x%08xnn“, return; in

15、t main() func(1,2,3); return 0; 编译后的执行结果是: 0x0012ff78 0x0012ff7c 0x0012ff80 0x0012ff68 0x0012ff6c 0x0012ff70 函数执行时的栈顶(ESP)、低端内存区域 var 1 var 2 var 3 RET “_cdecl”函数返回后的栈顶(ESP) parameter 1 parameter 2 parameter 3 “_stdcall”函数返回后的栈顶(ESP) 栈底(基地址 EBP)、高端内存区域 上图就是函数调用过程中堆栈的样子了。首先,三个参数以从又到左的次序压入堆栈,先 压“param

16、3”,再压“param2”,最后压入“param1”;然后压入函数的返回地址(RET), 接着跳转到函数地址接着执行(这里要补充一点,介绍 UNIX 下的缓冲溢出原理的文章中都 提到在压入 RET 后,继续压入当前 EBP,然后用当前 ESP 代替 EBP。然而,有一篇介绍 windows 下函数调用的文章中说,在 windows 下的函数调用也有这一步骤,但根据我的实 际调试,并未发现这一步,这还可以从 param3 和 var1 之间只有 4 字节的间隙这点看出来) ;第三步,将栈顶(ESP)减去一个数,为本地变量分配内存空间,上例中是减去 12 字节 (ESP=ESP-3*4,每个 in

17、t 变量占用 4 个字节);接着就初始化本地变量的内存空间。由于 “_stdcall”调用由被调函数调整堆栈,所以在函数返回前要恢复堆栈,先回收本地变量 占用的内存(ESP=ESP+3*4),然后取出返回地址,填入 EIP 寄存器,回收先前压入参数占用 的内存(ESP=ESP+3*4),继续执行调用者的代码。参见下列汇编代码: ;-func 函数的汇编代码- :00401000 83EC0C sub esp, 0000000C /创建本地变量的内存空间 :00401003 8B442410 mov eax, dword ptr esp+10 :00401007 8B4C2414 mov ecx

18、, dword ptr esp+14 :0040100B 8B542418 mov edx, dword ptr esp+18 :0040100F 89442400 mov dword ptr esp, eax :00401013 8D442410 lea eax, dword ptr esp+10 :00401017 894C2404 mov dword ptr esp+04, ecx (省略若干代码) :00401075 83C43C add esp, 0000003C ;恢复堆栈,回收本地变量的内存空间 :00401078 C3 ret 000C ;函数返回,恢复参数占用的内存空间 ;如

19、果是“_cdecl”的话,这里是“ret”,堆栈将由调用者恢复 ;-函数结束- ;-主程序调用 func 函数的代码- :00401080 6A03 push 00000003 /压入参数 param3 :00401082 6A02 push 00000002 /压入参数 param2 :00401084 6A01 push 00000001 /压入参数 param1 :00401086 E875FFFFFF call 00401000 /调用 func 函数 ;如果是“_cdecl”的话,将在这里恢复堆栈,“add esp, 0000000C” 聪明的读者看到这里,差不多就明白缓冲溢出的原理

20、了。先来看下面的代码: #include #include void _stdcall func() char lpBuff8=“0“; strcat(lpBuff,“AAAAAAAAAAA“); return; int main() func(); return 0; 编译后执行一下回怎么样?哈,“0x00414141“指令引用的“0x00000000“内存。该内存不 能为“read“。”,“非法操作”喽!“41“就是“A“的 16 进制的 ASCII 码了,那明显就是 strcat 这句出的问题了。“lpBuff“的大小只有 8 字节,算进结尾的0,那 strcat 最多只 能写入 7 个

21、“A“,但程序实际写入了 11 个“A“外加 1 个0。再来看看上面那幅图,多出来的 4 个字节正好覆盖了 RET 的所在的内存空间,导致函数返回到一个错误的内存地址,执行 了错误的指令。如果能精心构造这个字符串,使它分成三部分,前一部份仅仅是填充的无 意义数据以达到溢出的目的,接着是一个覆盖 RET 的数据,紧接着是一段 shellcode,那 只要着个 RET 地址能指向这段 shellcode 的第一个指令,那函数返回时就能执行 shellcode 了。但是软件的不同版本和不同的运行环境都可能影响这段 shellcode 在内存 中的位置,那么要构造这个 RET 是十分困难的。一般都在

22、RET 和 shellcode 之间填充大量 的 NOP 指令,使得 exploit 有更强的通用性。 低端内存区域 由 exploit 填入数据的开始 buffer 填入无用的数据 RET 指向 shellcode,或 NOP 指令的范围 NOP 填入的 NOP 指令,是 RET 可指向的范围 NOP shellcode 由 exploit 填入数据的结束 高端内存区域 windows 下的动态数据除了可存放在栈中,还可以存放在堆中。了解 C+的朋友都知道, C+可以使用 new 关键字来动态分配内存。来看下面的 C+代码: #include #include #include void f

23、unc() char *buffer=new char128; char bufflocal128; static char buffstatic128; printf(“0x%08xn“,buffer); /打印堆中变量的内存地址 printf(“0x%08xn“,bufflocal); /打印本地变量的内存地址 printf(“0x%08xn“,buffstatic); /打印静态变量的内存地址 void main() func(); return; 程序执行结果为: 0x004107d0 0x0012ff04 0x004068c0 可以发现用 new 关键字分配的内存即不在栈中,也不在静

24、态数据区。VC 编译器是通过 windows 下的“堆(heap)”来实现 new 关键字的内存动态分配。在讲“堆”之前,先来了 解一下和“堆”有关的几个 API 函数: HeapAlloc 在堆中申请内存空间 HeapCreate 创建一个新的堆对象 HeapDestroy 销毁一个堆对象 HeapFree 释放申请的内存 HeapWalk 枚举堆对象的所有内存块 GetProcessHeap 取得进程的默认堆对象 GetProcessHeaps 取得进程所有的堆对象 LocalAlloc GlobalAlloc 当进程初始化时,系统会自动为进程创建一个默认堆,这个堆默认所占内存的大小为 1

25、M。 堆对象由系统进行管理,它在内存中以链式结构存在。通过下面的代码可以通过堆动态申 请内存空间: HANDLE hHeap=GetProcessHeap(); char *buff=HeapAlloc(hHeap,0,8); 其中 hHeap 是堆对象的句柄,buff 是指向申请的内存空间的地址。那这个 hHeap 究竟是什 么呢?它的值有什么意义吗?看看下面这段代码吧: #pragma comment(linker,“/entry:main“) /定义程序的入口 #include _CRTIMP int (_cdecl *printf)(const char *, .); /定义 STL

26、函数 printf /*- 写到这里,我们顺便来复习一下前面所讲的知识: (*注)printf 函数是 C 语言的标准函数库中函数,VC 的标准函数库由 msvcrt.dll 模块实现。 由函数定义可见,printf 的参数个数是可变的,函数内部无法预先知道调用者压入的参数 个数,函数只能通过分析第一个参数字符串的格式来获得压入参数的信息,由于这里参数 的个数是动态的,所以必须由调用者来平衡堆栈,这里便使用了_cdecl 调用规则。 BTW,Windows 系统的 API 函数基本上是_stdcall 调用形式,只有一个 API 例外,那就是 wsprintf,它使用_cdecl 调用规则,同

27、 printf 函数一样,这是由于它的参数个数是可变 的缘故。 -*/ void main() HANDLE hHeap=GetProcessHeap(); char *buff=HeapAlloc(hHeap,0,0x10); char *buff2=HeapAlloc(hHeap,0,0x10); HMODULE hMsvcrt=LoadLibrary(“msvcrt.dll“); printf=(void *)GetProcAddress(hMsvcrt,“printf“); printf(“0x%08xn“,hHeap); printf(“0x%08xn“,buff); printf(

28、“0x%08xnn“,buff2); 执行结果为: 0x00130000 0x00133100 0x00133118 hHeap 的值怎么和那个 buff 的值那么接近呢?其实 hHeap 这个句柄就是指向 HEAP 首部的 地址。在进程的用户区存着一个叫 PEB(进程环境块)的结构,这个结构中存放着一些有关 进程的重要信息,其中在 PEB 首地址偏移 0x18 处存放的 ProcessHeap 就是进程默认堆的地 址,而偏移 0x90 处存放了指向进程所有堆的地址列表的指针。windows 有很多 API 都使用 进程的默认堆来存放动态数据,如 windows 2000 下的所有 ANSI

29、版本的函数都是在默认堆 中申请内存来转换 ANSI 字符串到 Unicode 字符串的。对一个堆的访问是顺序进行的,同一 时刻只能有一个线程访问堆中的数据,当多个线程同时有访问要求时,只能排队等待,这 样便造成程序执行效率下降。 最后来说说内存中的数据对齐。所位数据对齐,是指数据所在的内存地址必须是该数据长 度的整数倍,DWORD 数据的内存起始地址能被 4 除尽,WORD 数据的内存起始地址能被 2 除 尽,x86 CPU 能直接访问对齐的数据,当他试图访问一个未对齐的数据时,会在内部进行 一系列的调整,这些调整对于程序来说是透明的,但是会降低运行速度,所以编译器在编 译程序时会尽量保证数据

30、对齐。同样一段代码,我们来看看用 VC、Dev-C+和 lcc 三个不 同编译器编译出来的程序的执行结果: #include int main() int a; char b; int c; printf(“0x%08xn“, printf(“0x%08xn“, printf(“0x%08xn“, return 0; 这是用 VC 编译后的执行结果: 0x0012ff7c 0x0012ff7b 0x0012ff80 变量在内存中的顺序:b(1 字节)-a(4 字节)-c(4 字节)。 这是用 Dev-C+编译后的执行结果: 0x0022ff7c 0x0022ff7b 0x0022ff74 变量

31、在内存中的顺序:c(4 字节)-中间相隔 3 字节-b(占 1 字节)-a(4 字节)。 这是用 lcc 编译后的执行结果: 0x0012ff6c 0x0012ff6b 0x0012ff64 变量在内存中的顺序:同上。 三个编译器都做到了数据对齐,但是后两个编译器显然没 VC“聪明”,让一个 char 占了 4 字节,浪费内存哦。 基础知识: 堆栈是一种简单的数据结构,是一种只允许在其一端进行插入或删除的线性表。允许插入 或删除操作的一端称为栈顶,另一端称为栈底,对堆栈的插入和删除操作被称为入栈和出 栈。有一组 CPU 指令可以实现对进程的内存实现堆栈访问。其中,POP 指令实现出栈操作, P

32、USH 指令实现入栈操作。CPU 的 ESP 寄存器存放当前线程的栈顶指针,EBP 寄存器中保存 当前线程的栈底指针。CPU 的 EIP 寄存器存放下一个 CPU 指令存放的内存地址,当 CPU 执 行完当前的指令后,从 EIP 寄存器中读取下一条指令的内存地址,然后继续执行。 参考:Windows 下的 HEAP 溢出及其利用by: isno windows 核心编程by: Jeffrey Richter 摘要: 讨论常见的堆性能问题以及如何防范它们。(共 9 页) 前言 您是否是动态分配的 C/C+ 对象忠实且幸运的用户?您是否在模块间的往返通信中频繁地 使用了“自动化”?您的程序是否因堆

33、分配而运行起来很慢?不仅仅您遇到这样的问题。 几乎所有项目迟早都会遇到堆问题。大家都想说,“我的代码真正好,只是堆太慢”。那 只是部分正确。更深入理解堆及其用法、以及会发生什么问题,是很有用的。 什么是堆? (如果您已经知道什么是堆,可以跳到“什么是常见的堆性能问题?”部分) 在程序中,使用堆来动态分配和释放对象。在下列情况下,调用堆操作: 事先不知道程序所需对象的数量和大小。 对象太大而不适合堆栈分配程序。 堆使用了在运行时分配给代码和堆栈的内存之外的部分内存。下图给出了堆分配程序的不 同层。 GlobalAlloc/GlobalFree:Microsoft Win32 堆调用,这些调用直接

34、与每个进程的默认堆 进行对话。 LocalAlloc/LocalFree:Win32 堆调用(为了与 Microsoft Windows NT 兼容),这些调 用直接与每个进程的默认堆进行对话。 COM 的 IMalloc 分配程序(或 CoTaskMemAlloc / CoTaskMemFree):函数使用每个进程 的默认堆。自动化程序使用“组件对象模型 (COM)”的分配程序,而申请的程序使用每个 进程堆。 C/C+ 运行时 (CRT) 分配程序:提供了 malloc() 和 free() 以及 new 和 delete 操作 符。如 Microsoft Visual Basic 和 Ja

35、va 等语言也提供了新的操作符并使用垃圾收集来 代替堆。CRT 创建自己的私有堆,驻留在 Win32 堆的顶部。 Windows NT 中,Win32 堆是 Windows NT 运行时分配程序周围的薄层。所有 API 转发它 们的请求给 NTDLL。 Windows NT 运行时分配程序提供 Windows NT 内的核心堆分配程序。它由具有 128 个大 小从 8 到 1,024 字节的空闲列表的前端分配程序组成。后端分配程序使用虚拟内存来保 留和提交页。 在图表的底部是“虚拟内存分配程序”,操作系统使用它来保留和提交页。所有分配程序 使用虚拟内存进行数据的存取。 分配和释放块不就那么简单

36、吗?为何花费这么长时间? 堆实现的注意事项 传统上,操作系统和运行时库是与堆的实现共存的。在一个进程的开始,操作系统创建一 个默认堆,叫做“进程堆”。如果没有其他堆可使用,则块的分配使用“进程堆”。语言 运行时也能在进程内创建单独的堆。(例如,C 运行时创建它自己的堆。)除这些专用的 堆外,应用程序或许多已载入的动态链接库 (DLL) 之一可以创建和使用单独的堆。Win32 提供一整套 API 来创建和使用私有堆。有关堆函数(英文)的详尽指导,请参见 MSDN。 当应用程序或 DLL 创建私有堆时,这些堆存在于进程空间,并且在进程内是可访问的。从 给定堆分配的数据将在同一个堆上释放。(不能从一

37、个堆分配而在另一个堆释放。) 在所有虚拟内存系统中,堆驻留在操作系统的“虚拟内存管理器”的顶部。语言运行时堆 也驻留在虚拟内存顶部。某些情况下,这些堆是操作系统堆中的层,而语言运行时堆则通 过大块的分配来执行自己的内存管理。不使用操作系统堆,而使用虚拟内存函数更利于堆 的分配和块的使用。 典型的堆实现由前、后端分配程序组成。前端分配程序维持固定大小块的空闲列表。对于 一次分配调用,堆尝试从前端列表找到一个自由块。如果失败,堆被迫从后端(保留和提 交虚拟内存)分配一个大块来满足请求。通用的实现有每块分配的开销,这将耗费执行周 期,也减少了可使用的存储空间。 Knowledge Base 文章 Q

38、10758,“用 calloc() 和 malloc() 管理内存” (搜索文章编号) , 包含了有关这些主题的更多背景知识。另外,有关堆实现和设计的详细讨论也可在下列 著作中找到:“Dynamic Storage Allocation: A Survey and Critical Review”,作者 Paul R. Wilson、Mark S. Johnstone、Michael Neely 和 David Boles;“International Workshop on Memory Management”, 作者 Kinross, Scotland, UK, 1995 年 9 月 (

39、http:/www.cs.utexas.edu/users/oops/papers.html)(英文)。 Windows NT 的实现(Windows NT 版本 4.0 和更新版本) 使用了 127 个大小从 8 到 1,024 字节的 8 字节对齐块空闲列表和一个“大块”列表。“大块”列表(空闲列表0) 保存大于 1,024 字节的块。空闲列表容纳了用双向链表链接在一起的对象。默认情况下, “进程堆”执行收集操作。(收集是将相邻空闲块合并成一个大块的操作。)收集耗费了 额外的周期,但减少了堆块的内部碎片。 单一全局锁保护堆,防止多线程式的使用。(请参见 “Server Performanc

40、e and Scalability Killers”中的第一个注意事项, George Reilly 所著,在 “MSDN Online Web Workshop”上(站点: 局锁本质上是用来保护堆数据结构,防止跨多线程的随机存取。若堆操作太频繁,单一全 局锁会对性能有不利的影响。 什么是常见的堆性能问题? 以下是您使用堆时会遇到的最常见问题: 分配操作造成的速度减慢。光分配就耗费很长时间。最可能导致运行速度减慢原因是空闲 列表没有块,所以运行时分配程序代码会耗费周期寻找较大的空闲块,或从后端分配程序 分配新块。 释放操作造成的速度减慢。释放操作耗费较多周期,主要是启用了收集操作。收集期间,

41、每个释放操作“查找”它的相邻块,取出它们并构造成较大块,然后再把此较大块插入空 闲列表。在查找期间,内存可能会随机碰到,从而导致高速缓存不能命中,性能降低。 堆竞争造成的速度减慢。当两个或多个线程同时访问数据,而且一个线程继续进行之前必 须等待另一个线程完成时就发生竞争。竞争总是导致麻烦;这也是目前多处理器系统遇到 的最大问题。当大量使用内存块的应用程序或 DLL 以多线程方式运行(或运行于多处理器 系统上)时将导致速度减慢。单一锁定的使用常用的解决方案意味着使用堆的所有操 作是序列化的。当等待锁定时序列化会引起线程切换上下文。可以想象交叉路口闪烁的红 灯处走走停停导致的速度减慢。 竞争通常会

42、导致线程和进程的上下文切换。上下文切换的开销是很大的,但开销更大的是 数据从处理器高速缓存中丢失,以及后来线程复活时的数据重建。 堆破坏造成的速度减慢。造成堆破坏的原因是应用程序对堆块的不正确使用。通常情形包 括释放已释放的堆块或使用已释放的堆块,以及块的越界重写等明显问题。(破坏不在本 文讨论范围之内。有关内存重写和泄漏等其他细节,请参见 Microsoft Visual C+(R) 调 试文档 。) 频繁的分配和重分配造成的速度减慢。这是使用脚本语言时非常普遍的现象。如字符串被 反复分配,随重分配增长和释放。不要这样做,如果可能,尽量分配大字符串和使用缓冲 区。另一种方法就是尽量少用连接操

43、作。 竞争是在分配和释放操作中导致速度减慢的问题。理想情况下,希望使用没有竞争和快速 分配/释放的堆。可惜,现在还没有这样的通用堆,也许将来会有。 在所有的服务器系统中(如 IIS、MSProxy、DatabaseStacks、网络服务器、 Exchange 和 其他), 堆锁定实在是个大瓶颈。处理器数越多,竞争就越会恶化。 尽量减少堆的使用 现在您明白使用堆时存在的问题了,难道您不想拥有能解决这些问题的超级魔棒吗?我可 希望有。但没有魔法能使堆运行加快因此不要期望在产品出货之前的最后一星期能够大 为改观。如果提前规划堆策略,情况将会大大好转。调整使用堆的方法,减少对堆的操作 是提高性能的良方

44、。 如何减少使用堆操作?通过利用数据结构内的位置可减少堆操作的次数。请考虑下列实例: struct ObjectA / objectA 的数据 struct ObjectB / objectB 的数据 / 同时使用 objectA 和 objectB / / 使用指针 / struct ObjectB struct ObjectA * pObjA; / objectB 的数据 / / 使用嵌入 / struct ObjectB struct ObjectA pObjA; / objectB 的数据 / / 集合 在另一对象内使用 objectA 和 objectB / struct Objec

45、tX struct ObjectA objA; struct ObjectB objB; 避免使用指针关联两个数据结构。如果使用指针关联两个数据结构,前面实例中的对象 A 和 B 将被分别分配和释放。这会增加额外开销我们要避免这种做法。 把带指针的子对象嵌入父对象。当对象中有指针时,则意味着对象中有动态元素(百分之 八十)和没有引用的新位置。嵌入增加了位置从而减少了进一步分配/释放的需求。这将提 高应用程序的性能。 合并小对象形成大对象(聚合)。聚合减少分配和释放的块的数量。如果有几个开发者, 各自开发设计的不同部分,则最终会有许多小对象需要合并。集成的挑战就是要找到正确 的聚合边界。 内联缓

46、冲区能够满足百分之八十的需要(aka 80-20 规则)。个别情况下,需要内存缓冲 区来保存字符串/二进制数据,但事先不知道总字节数。估计并内联一个大小能满足百分之 八十需要的缓冲区。对剩余的百分之二十,可以分配一个新的缓冲区和指向这个缓冲区的 指针。这样,就减少分配和释放调用并增加数据的位置空间,从根本上提高代码的性能。 在块中分配对象(块化)。块化是以组的方式一次分配多个对象的方法。如果对列表的项 连续跟踪,例如对一个 名称,值 对的列表,有两种选择:选择一是为每一个“名称-值” 对分配一个节点;选择二是分配一个能容纳(如五个)“名称-值”对的结构。例如,一般 情况下,如果存储四对,就可减

47、少节点的数量,如果需要额外的空间数量,则使用附加的 链表指针。 块化是友好的处理器高速缓存,特别是对于 L1-高速缓存,因为它提供了增加的位置 不 用说对于块分配,很多数据块会在同一个虚拟页中。 正确使用 _amblksiz。C 运行时 (CRT) 有它的自定义前端分配程序,该分配程序从后端 (Win32 堆)分配大小为 _amblksiz 的块。将 _amblksiz 设置为较高的值能潜在地减少 对后端的调用次数。这只对广泛使用 CRT 的程序适用。 使用上述技术将获得的好处会因对象类型、大小及工作量而有所不同。但总能在性能和可 升缩性方面有所收获。另一方面,代码会有点特殊,但如果经过深思熟虑,代码还是很容 易管理的。 其他提高性能的技术 下面是一些提高速度的技术: 使

展开阅读全文
相关资源
相关搜索

当前位置:首页 > 实用文档资料库 > 策划方案

Copyright © 2018-2021 Wenke99.com All rights reserved

工信部备案号浙ICP备20026746号-2  

公安局备案号:浙公网安备33038302330469号

本站为C2C交文档易平台,即用户上传的文档直接卖给下载用户,本站只是网络服务中间平台,所有原创文档下载所得归上传人所有,若您发现上传作品侵犯了您的权利,请立刻联系网站客服并提供证据,平台将在3个工作日内予以改正。