堆、栈、BSS、Data、code区、静态存储区、⽂字常量区
在计算机领域,堆栈是⼀个不容忽视的概念,但是很多⼈甚⾄是计算机专业的⼈也没有明确堆栈其实是两种数据结构。
要点:
堆:顺序随意
栈:先进后出
堆和栈的区别
⼀、预备知识—程序的内存分配
⼀个由c/C++编译的程序占⽤的内存分为以下⼏个部分
1、栈区(stack)— 由编译器⾃动分配释放,存放函数的参数值,局部变量的值等。其操作⽅式类似于数据结构中的栈。
2、堆区(heap) — ⼀般由程序员分配释放,若程序员不释放(就会造成内存泄漏的问题),程序结束时可
能由OS回收。注意它与数据结构中的堆是两回事,分配⽅式倒是类似于链表,呵呵。
3、全局区(静态区)(static)—,全局变量和静态变量的存储是放在⼀块的,初始化的全局变量和静态变量在⼀块区域(data),未初始化的全局变量和未初始化的静态变量在相邻的另⼀块区域(BSS,Block Started by Symbol)。 - 程序结束后有系统释放(在整个程序的执⾏过程中都是有效的)
4、⽂字常量区 —常量字符串就是放在这⾥的。程序结束后由系统释放 (⽂字常量区内的数据可以修改吗?)
5、程序代码区(code)—存放函数体的⼆进制代码。
⼆、例⼦程序
这是⼀个前辈写的,⾮常详细
//main.cpp
int a = 0; 全局初始化区
char *p1; 全局未初始化区
main()
{
int b; 栈
char s[] = "abc"; 栈                  //abc是在栈⾥⾯,⽽下⾯的123456/0却在在常量区内,要注意这两种情况的区别
char *p2; 栈
char *p3 = "123456"; 123456/0在常量区,p3在栈上。
static int c =0;全局(静态)初始化区
p1 = (char *)malloc(10);
p2 = (char *)malloc(20);
分配得来得10和20字节的区域就在堆区。
strcpy(p1, "123456"); 123456/0放在常量区,编译器可能会将它与p3所指向的"123456"优化成⼀个地⽅。 (所谓的优化是什么意思,是指P1和P2指向的是同⼀块内存吗?)
}
//main.cpp
int a = 0;  // 全局初始化区
char *p1;  //全局未初始化区
main()
{
int b;                      //栈
char s[] = "abc";    //栈    //abc是在栈⾥⾯,⽽下⾯123456/0却在在常量区内,要注意这两种情况的区别
char *p2;              // 栈
char *p3 = "123456"; //123456/0在常量区,p3在栈上。
static int c =0;全局(静态)初始化区
p1 = (char *)malloc(10);
p2 = (char *)malloc(20);  //分配得来得10和20字节的区域就在堆区。
strcpy(p1, "123456"); //123456/0放在常量区,编译器可能会将它与p3所指向的"123456"优化成⼀个地⽅。 (所谓的优化是什么意思,是指P1和P2指向的是同⼀块内存吗?) }
⼆、堆和栈的理论知识
2.1申请⽅式字符常量池是什么意思
stack:
由系统⾃动分配。例如,声明在函数中⼀个局部变量 int b; 系统⾃动在栈中为b开辟空间
heap:
需要程序员⾃⼰申请,并指明⼤⼩,在c中malloc函数
如p1 = (char *)malloc(10);
在C++中⽤new运算符
如p2 = (char *)malloc(10);
但是注意p1、p2本⾝是在栈中的。
2.2 申请后系统的响应
栈:只要栈的剩余空间⼤于所申请空间,系统将为程序提供内存,否则将报异常提⽰栈溢出。
堆:⾸先应该知道操作系统有⼀个记录空闲内存地址的链表,当系统收到程序的申请时,
会遍历该链表,寻第⼀个空间⼤于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于⼤多数系统,会在这块内存空间中的⾸地址处记录本次分配的⼤⼩,这样,代码中的delete语句才能正确的释放本内存空间。另外,由于到的堆结点的⼤⼩不⼀定正好等于申请的⼤⼩,系统会⾃动的将多余的那部分重新放⼊空闲链表中。
2.3申请⼤⼩的限制
栈:在Windows下,栈是向低地址扩展的数据结构,是⼀块连续的内存的区域。这句话的意思是栈顶的
地址和栈的最⼤容量是系统预先规定好的,在 WINDOWS下,栈的⼤⼩是2M(也有的说是1M,总之是⼀个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提⽰overflow。因此,能从栈获得的空间较⼩。
堆:堆是向⾼地址扩展的数据结构,是不连续的内存区域。这是由于系统是⽤链表来存储的空闲内存地址的,⾃然是不连续的,⽽链表的遍历⽅向是由低地址向⾼地址。堆的⼤⼩受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间⽐较灵活,也⽐较⼤。
2.4申请效率的⽐较:
栈由系统⾃动分配,速度较快。但程序员是⽆法控制的。
堆是由new分配的内存,⼀般速度⽐较慢,⽽且容易产⽣内存碎⽚,不过⽤起来最⽅便.
另外,在WINDOWS下,最好的⽅式是⽤VirtualAlloc分配内存,他不是在堆,也不是在栈是直接在进程的地址空间中保留⼀快内存,虽然⽤起来最不⽅便。但是速度快,也最灵活
2.5堆和栈中的存储内容
栈:在函数调⽤时,第⼀个进栈的是主函数中后的下⼀条指令(函数调⽤语句的下⼀条可执⾏语句)的
地址,然后是函数的各个参数,在⼤多数的C编译器中,参数是由右往左⼊栈的,然后是函数中的局部变量。注意静态变量是不⼊栈的。当本次函数调⽤结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下⼀条指令,程序由该点继续运⾏。
堆:⼀般是在堆的头部⽤⼀个字节存放堆的⼤⼩。堆中的具体内容有程序员安排。
2.6存取效率的⽐较
char s1[] = "aaaaaaaaaaaaaaa";
char *s2 = "bbbbbbbbbbbbbbbbb";
aaaaaaaaaaa是在运⾏时刻赋值的;
⽽bbbbbbbbbbb是在编译时就确定的;
但是,在以后的存取中,在栈上的数组⽐指针所指向的字符串(例如堆)快。
⽐如:
#include
void main()
{
char a = 1;
char c[] = "1234567890";
char *p ="1234567890";
a = c[1];
a = p[1];
return;
}
#include
void main()
{
char a = 1;
char c[] = "1234567890";
char *p ="1234567890";
a = c[1];
a = p[1];
return;
}
对应的汇编代码
10: a = c[1];
00401067 8A 4D F1 mov cl,byte ptr [ebp-0Fh]
0040106A 88 4D FC mov byte ptr [ebp-4],cl
11: a = p[1];
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中,在根据edx读取字符,显然慢了。
2.7⼩结:
堆和栈的区别可以⽤如下的⽐喻来看出:
使⽤栈就象我们去饭馆⾥吃饭,只管点菜(发出申请)、付钱、和吃(使⽤),吃饱了就⾛,不必理会切菜、洗
菜等准备⼯作和洗碗、刷锅等扫尾⼯作,他的好处是快捷,但是⾃由度⼩。
使⽤堆就象是⾃⼰动⼿做喜欢吃的菜肴,⽐较⿇烦,但是⽐较符合⾃⼰的⼝味,⽽且⾃由度⼤。
堆和栈的区别主要分:
操作系统⽅⾯的堆和栈,如上⾯说的那些,不多说了。
还有就是数据结构⽅⾯的堆和栈,这些都是不同的概念。这⾥的堆实际上指的就是(满⾜堆性质的)优先队列的⼀种数据结构,第1个元素有最⾼的优先权;栈实际上就是满⾜先进后出的性质的数学或数据结构。
虽然堆栈,堆栈的说法是连起来叫,但是他们还是有很⼤区别的,连着叫只是由于历史的原因。
摘要:讨论常见的堆性能问题以及如何防范它们。
前⾔
您是否是动态分配的 C/C++ 对象忠实且幸运的⽤户?您是否在模块间的往返通信中频繁地使⽤了“⾃动化”?您的程序是否因堆分配⽽运⾏起来很慢?不仅仅您遇到这样的问题。⼏乎所有项⽬迟早都会遇到堆问题。⼤家都想说,“我的代码真正好,只是堆太慢”。那只是部分正确。更深⼊理解堆及其⽤法、以及会发⽣什么问题,是很有⽤的。
什么是堆?
(如果您已经知道什么是堆,可以跳到“什么是常见的堆性能问题?”部分)
在程序中,使⽤堆来动态分配和释放对象。在下列情况下,调⽤堆操作:
事先不知道程序所需对象的数量和⼤⼩。
对象太⼤⽽不适合堆栈分配程序。
堆使⽤了在运⾏时分配给代码和堆栈的内存之外的部分内存。下图给出了堆分配程序的不同层。
GlobalAlloc/GlobalFree:Microsoft Win32 堆调⽤,这些调⽤直接与每个进程的默认堆进⾏对话。
LocalAlloc/LocalFree:Win32 堆调⽤(为了与 Microsoft Windows NT 兼容),这些调⽤直接与每个进程的默认堆进⾏对话。
COM 的 IMalloc 分配程序(或 CoTaskMemAlloc / CoTaskMemFree):函数使⽤每个进程的默认堆。⾃动化程序使⽤“组件对象模型(COM)”的分配程序,⽽申请的程序使⽤每个进程堆。
C/C ++ 运⾏时 (CRT) 分配程序:提供了 malloc() 和 free() 以及 new 和 delete 操作符。如 Microsoft Vis
ual Basic 和 Java 等语⾔也提供了新的操作符并使⽤垃圾收集来代替堆。CRT 创建⾃⼰的私有堆,驻留在 Win32 堆的顶部。
Windows NT 中,Win32 堆是 Windows NT 运⾏时分配程序周围的薄层。所有 API 转发它们的请求给 NTDLL。
Windows NT 运⾏时分配程序提供 Windows NT 内的核⼼堆分配程序。它由具有 128 个⼤⼩从 8 到 1,024 字节的空闲列表的前端分配程序组成。后端分配程序使⽤虚拟内存来保留和提交页。
在图表的底部是“虚拟内存分配程序”,操作系统使⽤它来保留和提交页。所有分配程序使⽤虚拟内存进⾏数据的存取。
分配和释放块不就那么简单吗?为何花费这么长时间?
堆实现的注意事项
传统上,操作系统和运⾏时库是与堆的实现共存的。在⼀个进程的开始,操作系统创建⼀个默认堆,叫做“进程堆”。如果没有其他堆可使⽤,则块的分配使⽤“进程堆”。语⾔运⾏时也能在进程内创建单独的堆。(例如,C 运⾏时创建它⾃⼰的堆。)除这些专⽤的堆外,应⽤程序或许多已载⼊的动态链接库 (DLL) 之⼀可以创建和使⽤单独的堆。Win32 提供⼀整套 API 来创建和使⽤私有堆。有关堆函数(英⽂)
的详尽指导,请参见 MSDN。
当应⽤程序或 DLL 创建私有堆时,这些堆存在于进程空间,并且在进程内是可访问的。从给定堆分配的数据将在同⼀个堆上释放。(不能从⼀个堆分配⽽在另⼀个堆释放。)
在所有虚拟内存系统中,堆驻留在操作系统的“虚拟内存管理器”的顶部。语⾔运⾏时堆也驻留在虚拟内存顶部。某些情况下,这些堆是操作系统堆中的层,⽽语⾔运⾏时堆则通过⼤块的分配来执⾏⾃⼰的内存管理。不使⽤操作系统堆,⽽使⽤虚拟内存函数更利于堆的分配和块的使⽤。
典型的堆实现由前、后端分配程序组成。前端分配程序维持固定⼤⼩块的空闲列表。对于⼀次分配调⽤,堆尝试从前端列表到⼀个⾃由块。如果失败,堆被迫从后端(保留和提交虚拟内存)分配⼀个⼤块来满⾜请求。通⽤的实现有每块分配的开销,这将耗费执⾏周期,也减少了可使⽤的存储空间。
Knowledge Base ⽂章 Q10758,“⽤ 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 ⽉()(英⽂)。
Windows NT 的实现(Windows NT 版本 4.0 和更新版本)使⽤了 127 个⼤⼩从 8 到 1,024 字节的 8 字节对齐块空闲列表和⼀个“⼤块”列表。“⼤块”列表(空闲列表[0])保存⼤于 1,024 字节的块。空闲列表容纳了⽤双向链表链接在⼀起的对象。默认情况下,“进程堆”执⾏收集操作。(收集是将相邻空闲块合并成⼀个⼤块的操作。)收集耗费了额外的周期,但减少了堆块的内部碎⽚。
单⼀全局锁保护堆,防⽌多线程式的使⽤。(请参见“Server Performance and Scalability Killers”中的第⼀个注意事项, George Reilly 所著,在 “MSDN Online Web Workshop”上(站点:(英⽂)。)单⼀全局锁本质上是⽤来保护堆数据结构,防⽌跨多线程的随机存取。若堆操作
太频繁,单⼀全局锁会对性能有不利的影响。
什么是常见的堆性能问题?
以下是您使⽤堆时会遇到的最常见问题:
分配操作造成的速度减慢。光分配就耗费很长时间。最可能导致运⾏速度减慢原因是空闲列表没有块,所以运⾏时分配程序代码会耗费周期寻较⼤的空闲块,或从后端分配程序分配新块。
释放操作造成的速度减慢。释放操作耗费较多周期,主要是启⽤了收集操作。收集期间,每个释放操作“查”它的相邻块,取出它们并构造成较⼤块,然后再把此较⼤块插⼊空闲列表。在查期间,内存
可能会随机碰到,从⽽导致⾼速缓存不能命中,性能降低。
堆竞争造成的速度减慢。当两个或多个线程同时访问数据,⽽且⼀个线程继续进⾏之前必须等待另⼀个线程完成时就发⽣竞争。竞争总是导致⿇烦;这也是⽬前多处理器系统遇到的最⼤问题。当⼤量使⽤内存块的应⽤程序或 DLL 以多线程⽅式运⾏(或运⾏于多处理器系统上)时将导致速度减慢。单⼀锁定的使⽤—常⽤的解决⽅案—意味着使⽤堆的所有操作是序列化的。当等待锁定时序列化会引起线程切换上下⽂。可以想象交叉路⼝闪烁的红灯处⾛⾛停停导致的速度减慢。
竞争通常会导致线程和进程的上下⽂切换。上下⽂切换的开销是很⼤的,但开销更⼤的是数据从处理器⾼速缓存中丢失,以及后来线程复活时的数据重建。
堆破坏造成的速度减慢。造成堆破坏的原因是应⽤程序对堆块的不正确使⽤。通常情形包括释放已释放的堆块或使⽤已释放的堆块,以及块的越界重写等明显问题。(破坏不在本⽂讨论范围之内。有关内存重写和泄漏等其他细节,请参见 Microsoft Visual C++(R) 调试⽂档。)
频繁的分配和重分配造成的速度减慢。这是使⽤脚本语⾔时⾮常普遍的现象。如字符串被反复分配,随重分配增长和释放。不要这样做,如果可能,尽量分配⼤字符串和使⽤缓冲区。另⼀种⽅法就是尽量少⽤连接操作。
竞争是在分配和释放操作中导致速度减慢的问题。理想情况下,希望使⽤没有竞争和快速分配/释放的堆。可惜,现在还没有这样的通⽤堆,也许将来会有。
在所有的服务器系统中(如 IIS、MSProxy、DatabaseStacks、⽹络服务器、 Exchange 和其他), 堆锁定实在是个⼤瓶颈。处理器数越多,竞争就越会恶化。
尽量减少堆的使⽤
现在您明⽩使⽤堆时存在的问题了,难道您不想拥有能解决这些问题的超级魔棒吗?我可希望有。但没有魔法能使堆运⾏加快—因此不要期望在产品出货之前的最后⼀星期能够⼤为改观。如果提前规划堆策略,情况将会⼤⼤好转。调整使⽤堆的⽅法,减少对堆的操作是提⾼性能的良⽅。
如何减少使⽤堆操作?通过利⽤数据结构内的位置可减少堆操作的次数。请考虑下列实例:
struct ObjectA {
// objectA 的数据
}
struct ObjectB {
// objectB 的数据
}
// 同时使⽤ objectA 和 objectB
//
// 使⽤指针
//
struct ObjectB {
struct ObjectA * pObjA;
// objectB 的数据
}
//
/
/ 使⽤嵌⼊
//
struct ObjectB {
struct ObjectA pObjA;
// objectB 的数据
}
//
// 集合 – 在另⼀对象内使⽤ objectA 和 objectB
//
struct ObjectX {
struct ObjectA  objA;
struct ObjectB  objB;
}
struct ObjectA {
// objectA 的数据
}
struct ObjectB {
// objectB 的数据
}
// 同时使⽤ objectA 和 objectB
//
// 使⽤指针
/
/
struct ObjectB {
struct ObjectA * pObjA;
// objectB 的数据
}
//
// 使⽤嵌⼊
//
struct ObjectB {
struct ObjectA pObjA;
// objectB 的数据
}
//
// 集合 – 在另⼀对象内使⽤ objectA 和 objectB
//
struct ObjectX {
struct ObjectA  objA;
struct ObjectB  objB;
}
避免使⽤指针关联两个数据结构。如果使⽤指针关联两个数据结构,前⾯实例中的对象 A 和 B 将被分别分配和释放。这会增加额外开销—我们要避免这种做法。
把带指针的⼦对象嵌⼊⽗对象。当对象中有指针时,则意味着对象中有动态元素(百分之⼋⼗)和没有引⽤的新位置。嵌⼊增加了位置从⽽减少了进⼀步分配/释放的需求。这将提⾼应⽤程序的性能。
合并⼩对象形成⼤对象(聚合)。聚合减少分配和释放的块的数量。如果有⼏个开发者,各⾃开发设计的不同部分,则最终会有许多⼩对象需要合并。集成的挑战就是要到正确的聚合边界。
内联缓冲区能够满⾜百分之⼋⼗的需要(aka 80-20 规则)。个别情况下,需要内存缓冲区来保存字符串/⼆进制数据,但事先不知道总字节数。估计并内联⼀个⼤⼩能满⾜百分之⼋⼗需要的缓冲区。对剩余的百分之⼆⼗,可以分配⼀个新的缓冲区和指向这个缓冲区的指针。这样,就减少分配和释放调⽤并增加数据的位置空间,从根本上提⾼代码的性能。
在块中分配对象(块化)。块化是以组的⽅式⼀次分配多个对象的⽅法。如果对列表的项连续跟踪,例如对⼀个 {名称,值} 对的列表,有两种选择:选择⼀是为每⼀个“名称-值”对分配⼀个节点;选择⼆是分配⼀个能容纳(如五个)“名称-值”对的结构。例如,⼀般情况下,如果存储四对,就可减少节点的数量,如果需要额外的空间数量,则使⽤附加的链表指针。
块化是友好的处理器⾼速缓存,特别是对于 L1-⾼速缓存,因为它提供了增加的位置 —不⽤说对于块分配,很多数据块会在同⼀个虚拟页中。
正确使⽤ _amblksiz。C 运⾏时 (CRT) 有它的⾃定义前端分配程序,该分配程序从后端(Win32 堆)分配⼤⼩为 _amblksiz 的块。将
_amblksiz 设置为较⾼的值能潜在地减少对后端的调⽤次数。这只对⼴泛使⽤ CRT 的程序适⽤。
使⽤上述技术将获得的好处会因对象类型、⼤⼩及⼯作量⽽有所不同。但总能在性能和可升缩性⽅⾯有所收获。另⼀⽅⾯,代码会有点特殊,但如果经过深思熟虑,代码还是很容易管理的。
其他提⾼性能的技术
下⾯是⼀些提⾼速度的技术:
使⽤ Windows NT5 堆
由于⼏个同事的努⼒和⾟勤⼯作,1998 年初 Microsoft Windows(R) 2000 中有了⼏个重⼤改进:
改进了堆代码内的锁定。堆代码对每堆⼀个锁。全局锁保护堆数据结构,防⽌多线程式的使⽤。但不幸的是,在⾼通信量的情况下,堆仍受困于全局锁,导致⾼竞争和低性能。Windows 2000 中,锁内代码的临界区将竞争的可能性减到最⼩,从⽽提⾼了可伸缩性。
使⽤ “Lookaside”列表。堆数据结构对块的所有空闲项使⽤了⼤⼩在 8 到 1,024 字节(以 8-字节递增)的快速⾼速缓存。快速⾼速缓存最初

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。