导语 深入理解C++内存管理,一文了解所有C++内存问题,万字长文,建议收藏 C语言与CPP编程 分享C语言/C++,数据结构与算法,计算机基础,操作系统等 46篇原创内容 公众号 随着人工智能,云计算等技术的迅猛发展,让Python,go等新兴语言流行了起来,很多人以为C++可能已经过时了,确实,C++编程语言走到今天已经有将近40年的历史了,但它依然是当今的主流语言,我们可以看一下世界权威编程语言排行榜,C++依然是属于第一梯队,C++在金融交易系统,游戏,数据库,编译器,大型桌面程序,高性能服务器,浏览器,各类编程比赛(ACM-ICPC,Topcoder,Codeforces,Google Code Jam)等领域任然是主力军。 在各个大厂情况,C++也是很多大厂主力编程语言,国外google和微软大部分核心产品都是基于C++开发的;鹅厂编程语言TOP5,C++排第一: C++的高抽象层次,又兼具高性能,是其他语言所无法替代的,C++标准保持稳定发展,更加现代化,更加强大,更加易用,熟练的 C++ 工程师自然也获得了“高水平、高薪资”的名声,但在各种活跃编程语言中,C++门槛依然很高,尤其C++的内存问题(内存泄露,内存溢出,内存宕机,堆栈破坏等问题),需要理解C++标准对象模型,C++标准库,标准C库,操作系统等内存设计,才能更加深入理解C++内存管理,这是跨越C++三座大山之一,我们必须拿下它。 Content环境: uname -a Linux alexfeng 3.19.0-15-generic #15-Ubuntu SMP Thu Apr 16 23:32:37 UTC 2015 x86_64 x86_64 x86_64 GNU/Linux cat /proc/cpuinfo bugs : bogomips : 4800.52 clflush size : 64 cache_alignment : 64 address sizes : 36 bits physical, 48 bits virtual cat /proc/meminfo MemTotal: 4041548 kB(4G) MemFree: 216304 kB MemAvailable: 2870340 kB Buffers: 983360 kB Cached: 1184008 kB SwapCached: 54528 kB GNU gdb (Ubuntu 7.9-1ubuntu1) 7.9 g++ (Ubuntu 4.9.2-10ubuntu13) 4.9.2 一 C++内存模型C++11在标准库中引入了memory model,这应该是C++11最重要的特性之一了。C++11引入memory model的意义在于我们可以在high level language层面实现对在多处理器中多线程共享内存交互的控制。我们可以在语言层面忽略compiler,CPU arch的不同对多线程编程的影响了。我们的多线程可以跨平台。 内存模型为 C++ 定义计算机内存存储的语义。可用于 C++ 程序的内存是一或多个相接的字节序列。内存中的每个字节拥有唯一的地址。 字节字节是最小的可寻址内存单元。它被定义为相接的位序列,大到足以保有任何 内存位置内存位置是
注意:各种语言特性,例如引用和虚函数,可能涉及到程序不可访问,但为实现所管理的额外内存位置。 线程与数据竞争
一个表达式的求值写入内存位置,而另一求值读或写同一内存位置时,称这些表达式冲突。拥有二个冲突求值的程序有数据竞争,除非
若出现数据竞争,则程序的行为未定义。 内存顺序(std::memory_order)如果不使用任何同步机制(例如 mutex 或 atomic),在多线程中读写同一个变量,那么程序的结果是难以预料的。简单来说,编译器以及 CPU 的一些行为,会影响到C++程序的执行结果
多线程读写同一变量需要使用同步机制,最常见的同步机制就是 C++11 提供6 种可以应用于原子变量的内存次序:
虽然共有 6 个选项,但它们表示的是四种内存模型:
顺序一致次序(sequential consisten ordering) 对应memory_order_seq_cst. SC作为默认的内存序,是因为它意味着将程序看做是一个简单的序列。如果对于一个原子变量的操作都是顺序一致的,那么多线程程序的行为就像是这些操作都以一种特定顺序被单线程程序执行。从同的角度来看,一个顺序一致的 store 操作 synchroniezd-with 一个顺序一致的需要读取相同的变量的 load 操作。除此以外,顺序模型还保证了在 load 之后执行的顺序一致原子操作都得表现得在 store 之后完成。非顺序一致内存次序(non-sequentially consistency memory ordering)强调对同一事件(代码),不同线程可以以不同顺序去执行,不仅是因为编译器可以进行指令重排,也因为不同的 CPU cache 及内部缓存的状态可以影响这些指令的执行。但所有线程仍需要对某个变量的连续修改达成顺序一致。 松弛次序(relaxed ordering) 在这种模型下, 获取-释放次序(acquire-release ordering)
数据依赖(Release-Consume ordering) memory_order_consume 是 acquire-release 顺序模型中的一种,但它比较特殊,它为 inter-thread happens-before 引入了数据依赖关系:dependency-ordered-before ,一个使用memory_order_consume的操作具有消费语义(consume semantics)。我们称这个操作为消费操作(consume operations),对于memory_order_consume最的价值的观察结果就是总是可以安全的将它替换成memory_order_acquire,消费和获取都为了同一个目的:帮助非原子信息在线程间安全的传递。就像获取操作一样,消费操作必须与另一个线程的释放操作一起使用。它们之间主要的区别在于消费操作可以正确起作用的案例更少。相对于它的使用不便,反过来也就意味着消费操作在某些平台使用更有效。 默认情况下, 思考问题: 1 C++正常程序可以访问到哪些内存和不能访问到哪些内存(这些内存属于该程序)? 2 内存对程序并发执行有什么影响? 3 std::memory_order 的作用是什么? 二 C++对象内存模型1 空类对象(一般作为模板的tag来使用)class A { }; sizeof(A) = 1 C++标准要求C++的对象大小不能为0,C++对象必须在内存里面有唯一的地址, 但又不想浪费太多内存空间,所以标准规定为1byte, 2 非空类
3 非空虚基类
4 单继承
5 简单多继承class A { public: int a; virtual void v(); }; class B { public: int b; virtual void w(); }; class C : public A, public B { public: int c; };
sizeof(C) = 32 ,align = 8 6 简单多继承-2
7 The Diamond: 多重继承 (没有虚继承)
注意点:此种继承存在两份基类成员,使用时候需要指定路径,不方便,易出错。 8 The Diamond: 钻石类虚继承解决上面的问题,让基类只有存在一份,共享基类;class A { public: int a; virtual void v(); };
class B : public virtual A { public: int b; virtual void w(); };
class C : public virtual A { public: int c; virtual void x(); };
class D : public B, public C { public: int d; virtual void y(); };
sizeof(D) = 48,align = 8 注意点: 1.top_offset 表示this指针对子类的偏移,用于子类和继承类之间dynamic_cast转换(还需要typeinfo数据),实现多态, vbase_offset 表示this指针对基类的偏移,用于共享基类; 2.gcc为了每一个类生成一个vtable虚函数表,放在程序的.rodata段,其他编译器(平台)比如vs,实现不太一样. 3.gcc还有VTT表,里面存放了各个基类之间虚函数表的关系,最大化利用基类的虚函数表,专门用来为构建最终类vtable; 4.在构造函数里面设置对象的vtptr指针。 5.虚函数表地址的前面设置了一个指向type_info的指针,RTTI(Run Time Type Identification)运行时类型识别是有编译器在编译器生成的特殊类型信息,包括对象继承关系,对象本身的描述,RTTI是为多态而生成的信息,所以只有具有虚函数的对象在会生成。 6.在C++类中有两种成员数据:static、nonstatic;三种成员函数:static、nonstatic、virtual。 C++成员非静态数据需要占用动态内存,栈或者堆中,其他static数据存在全局变量区(数据段),编译时候确定。虚函数会增加用虚函数表大小,也是存储在数据区的.rodada段,编译时确定,其他函数不占空间。 7.G++选项 -fdump-class-hierarchy 可以生成C++类层结构,虚函数表结构,VTT表结构。 8.GDB调试选项: set p obj <on/off> :在C++中,如果一个对象指针指向其派生类, 如果打开这个选项,GDB会现在类对象结构的规则显示输出。 set p pertty <on/off>: 按照层次打印结构体。 思考问题: 1 Why don't we have virtual constructors? From Bjarne Stroustrup's C++ Style and Technique FAQ A virtual call is a mechanism to get work done given partial information. In particular, 'virtual' allows us to call a function knowing only any interfaces and not the exact type of the object. To create an object you need complete information. In particular, you need to know the exact type of what you want to create. Consequently, a 'call to a constructor' cannot be virtual. 2 为什么不要在构造函数或者析构函数中调用虚函数? 对于构造函数:此时子类的对象还没有完全构造,编译器会去虚函数化,只会用当前类的函数, 如果是纯虚函数,就会调用到纯虚函数,会导致构造函数抛异常:pure virtual method calle;对于析构函数:同样,由于对象不完整,编译器会去虚函数化,函数调用本类的虚函数,如果本类虚函数是纯虚函数,就会到账析构函数抛出异常: pure virtual method called; 3 C++对象构造顺序? 1.构造子类构造函数的参数 2.子类调用基类构造函数 3.基类设置vptr 4.基类初始化列表内容进行构造 5. 基类函数体调用 6. 子类设置vptr 7. 子类初始化列表内容进行构造 8. 子类构造函数体调用 4 为什么虚函数会降低效率? 是因为虚函数调用执行过程中会跳转两次,首先找到虚函数表,然后再查找对应函数地址,这样CPU指令就会跳转两次,而普通函数指跳转一次,CPU每跳转一次,预取指令都可能作废,这会导致分支预测失败,流水线排空,所以效率会变低。设想一下,如果说不是虚函数,那么在编译时期,其相对地址是确定的,编译器可以直接生成jmp/invoke指令;如果是虚函数,多出来的一次查找vtable所带来的开销,倒是次要的,关键在于,这个函数地址是动态的,譬如 取到的地址在eax里,则在call eax之后的那些已经被预取进入流水线的所有指令都将失效。流水线越长,一次分支预测失败的代价也就越大。 三 C++程序运行内存空间模型1. C++程序大致运行内存空间:32位: 64位: 2 Linux虚拟内存内部实现关键点:1 各个分区的意义 内核空间:在32位系统中,Linux会留1G空间给内核,用户进程是无法访问的,用来存放进程相关数据和内存数据,内核代码等;在64位系统里面,Linux会采用最低48位来表示虚拟内存,这可通过 /proc/cpuinfo 来查看address sizes : address sizes : 36 bits physical, 48 bits virtual,总的虚拟地址空间为256TB( 2^48 ),在这256TB的虚拟内存空间中, 0000000000000000 - 00007fffffffffff(128TB)为用户空间,ffff800000000000 - ffffffffffffffff(128TB)为内核空间。目前常用的分配设计: Virtual memory map with 4 level page tables: 0000000000000000 - 00007fffffffffff (=47 bits) user space, different per mm hole caused by [47:63] sign extension ffff800000000000 - ffff87ffffffffff (=43 bits) guard hole, reserved for hypervisor ffff880000000000 - ffffc7ffffffffff (=64 TB) direct mapping of all phys. memory ffffc80000000000 - ffffc8ffffffffff (=40 bits) hole ffffc90000000000 - ffffe8ffffffffff (=45 bits) vmalloc/ioremap space ffffe90000000000 - ffffe9ffffffffff (=40 bits) hole ffffea0000000000 - ffffeaffffffffff (=40 bits) virtual memory map (1TB) ... unused hole ... ffffec0000000000 - fffffbffffffffff (=44 bits) kasan shadow memory (16TB) ... unused hole ... vaddr_end for KASLR fffffe0000000000 - fffffe7fffffffff (=39 bits) cpu_entry_area mapping fffffe8000000000 - fffffeffffffffff (=39 bits) LDT remap for PTI ffffff0000000000 - ffffff7fffffffff (=39 bits) %esp fixup stacks ... unused hole ... ffffffef00000000 - fffffffeffffffff (=64 GB) EFI region mapping space ... unused hole ... ffffffff80000000 - ffffffff9fffffff (=512 MB) kernel text mapping, from phys 0 ffffffffa0000000 - fffffffffeffffff (1520 MB) module mapping space [fixmap start] - ffffffffff5fffff kernel-internal fixmap range ffffffffff600000 - ffffffffff600fff (=4 kB) legacy vsyscall ABI ffffffffffe00000 - ffffffffffffffff (=2 MB) unused hole http://www./doc/Documentation/x86/x86_64/mm.txt 剩下的是用户内存空间:
2 为了防止内存被攻击,比如栈溢出攻击和堆溢出攻击等,Linux在特定段之间使用随机偏移,使段的起始地址是随机值, Linux 系统上的ASLR 等级可以通过文件 /proc/sys/kernel/randomize_va_space 来进行设置,它支持以下取值:
3 每个段都有特定的安全控制(权限):
4 Linux虚拟内存是按页分配,每页大小为4KB或者2M,1G等(大页内存), 默认是4K; 5 例子-通过pmap 查看程序内存布局(综合proc/x/maps与proc/x/smaps数据):
g++ -g -std=c++11 -o main mem.cpp ./main 关闭内存地址随机化
思考问题: 1 栈为什么要由高地址向低地址扩展,堆为什么由低地址向高地址扩展?
2 如何查看进程虚拟地址空间的使用情况? 3 对比堆和栈优缺点? 四 C++栈内存空间模型
函数调用过程中,栈(有俗称堆栈)的变化: from https://zhuanlan.zhihu.com/p/25816426
2. 子函数执行:
3. 子函数调用返回:
在AT&T中: 以上两条指令可以被leave指令取代
栈攻击由上面栈内存布局可以看出,栈很容易被破坏和攻击,通过栈缓冲器溢出攻击,用攻击代码首地址来替换函数帧的返回地址,当子函数返回时,便跳转到攻击代码处执行,获取系统的控制权,所以操作系统和编译器采用了一些常用的防攻击的方法:
gcc关于栈溢出检测的几个参数: 开启Canary之后,函数开始时在ebp和临时变量之间插入一个随机值,函数结束时验证这个值。如果不相等(也就是这个值被其他值覆盖了),就会调用 _stackchk_fail函数,终止进程。对应GCC编译选项
栈异常处理
思考问题: 1 递归调用函数怎么从20层直接返回到17层,程序可以正常运行? 参考上面栈帧的结构,中心思想是当递归函数执行到第20层的时候,把当前栈帧的rbp值替换为17层的rbp的值, 怎么得到17层rbp的值, 就是通过反复取rbp的值(rbp保持了上一帧的rbp),核心代码如下: /*change stack*/ int ret_stack(int layer) { unsigned long rbp = 0; unsigned long layer_rbp = 0; int depth = 0; /* 1.得到首层函数的栈基址 */ __asm__ volatile( 'movq %%rbp, %0 \n\t' :'=r'(rbp) : :'memory'); layer_rbp = rbp; cout << hex<< rbp <<endl; /* 2.逐层回溯栈基址 */ for(; (depth < layer) && (0 != layer_rbp) && (0 != *(unsigned long *)layer_rbp) && (layer_rbp != *(unsigned long *)layer_rbp); ++depth) { cout << hex<< layer_rbp <<endl; layer_rbp = *(unsigned long *)layer_rbp; } cout << hex<< layer_rbp <<endl; //change current rbp to target layer rbp unsigned long *x = (unsigned long *)rbp; *x = layer_rbp; cout << hex<< x << ' v:' << *x <<endl; return depth; }
2 调用约定有哪些? 我们最常用是以下几种约定 1. cdec 是c/c++默认的调用约定 它是微软Win32 API的一准标准,我们常用的回调函数就是通过这种调用方式 3. thiscall thiscall 是c++中非静态类成员函数的默认调用约定 五 C++堆内存空间模型 1. C++ 程序动态申请内存new/delete:new/delete 操作符,C++内置操作符1. new操作符做两件事,分配内存+调用构造函数初始化。你不能改变它的行为; 2. delete操作符同样做两件事,调用析构函数+释放内存。你不能改变它的行为; operator new/delete 函数operator new : The default allocation and deallocation functions are special components of the standard library; They have the following unique properties:
If set_new_handler has been used to define anew_handler function, this new-handler function is called by the default definitions of the allocating versions ((1) and (2)) if they fail to allocate the requested storage. from http://www./reference/new/operator%20new/ 1.是用来专门分配内存的函数,为new操作符调用,你能增加额外的参数重载函数operator new(有限制):
2.operator new 底层一般调用malloc函数(gcc+glibc)分配内存; 3.operator new 分配失败会抛异常(默认),通过传递参数也可以不抛异常,返回空指针; operator delete : 1.是用来专门分配内存的函数,为delete操作符调用,你能增加额外的参数重载函数operator delete(有限制):
2.operator delete底层一般调用free函数(gcc+glibc)释放内存; 3.operator delete分配失败会抛异常(默认),通过传递参数也可以不抛异常,返回空指针; placement new/delete 函数1. placement new 其实就是new的一种重载,placement new是一种特殊的operator new,作用于一块已分配但未处理或未初始化的raw内存,就是用一块已经分配好的内存上重建对象(调用构造函数); 2. 它是C++库标准的一部分; 3. placement delete 什么都不做; 4. 数组分配 new[]/delete[] 表达式
http://www./reference/new/operator%20new[]/ class-specific allocation functions(成员函数)
定制对象特殊new/delete函数; 实现一般是使用全局: ::operator new ::operator delete 关键点:
思考问题:1 malloc和free是怎么实现的? 2 malloc 分配多大的内存,就占用多大的物理内存空间吗? 3 free 的内存真的释放了吗(还给 OS ) ? 4 既然堆内内存不能直接释放,为什么不全部使用 mmap 来分配? 5 如何查看堆内内存的碎片情况? 6 除了 glibc 的 malloc/free ,还有其他第三方实现吗? 2. C++11的智能指针与垃圾回收
新的智能指针: 1. shared_ptr
2. unique_ptr独占指针,不共享,不能赋值拷贝; unique_ptr关键点:
3. weak_ptr
更详细参考: http://en./w/cpp/memory/shared_ptr
所谓异常安全是指,当异常抛出时,带有异常安全的函数会:
智能指针就是采用RAII技术,即以对象管理资源来防止资源泄漏。
5 智能指针是线程安全的吗?
https://www./doc/libs/1_67_0/libs/smart_ptr/doc/html/smart_ptr.html#shared_ptr_thread_safety C++标准垃圾回收C++11 提供最小垃圾支持 declare_reachable 由于很多场景受限,当前几乎没有人使用; 感兴趣可以参考: http://www./C++11FAQ.html#gc-abi http://www./jtc1/sc22/wg21/docs/papers/2008/n2585.pdf 思考问题: 1 C++可以通过哪些技术来支持“垃圾回收”? smart_ptr,RAII, move语义等; 2 RAII是指什么?
from https://zh./wiki/RAII3. C++ STL 内存模型STL(C++标准模板库)引入的一个Allocator概念。整个STL所有组件的内存均从allocator分配。也就是说,STL并不推荐使用 new/delete 进行内存管理,而是推荐使用allocator。 SGI STL allocator总体设计: 对象的构造和析构采用placement new函数: 内存配置: 分配算法: 思考问题: 1. vector内存设计和array的区别和适用的场景? 2. 遍历map与遍历vector哪个更快,为什么? 3. STL的map和unordered_map内存设计各有什么不同? 六 C++内存问题及常用的解决方法1. 内存管理功能问题由于C++语言对内存有主动控制权,内存使用灵活和效率高,但代价是不小心使用就会导致以下内存错误: · memory overrun:写内存越界 常用的解决内存错误的方法
静态代码检测是指无需运行被测代码,通过词法分析、语法分析、控制流、数据流分析等技术对程序代码进行扫描,找出代码隐藏的错误和缺陷,如参数不匹配,有歧义的嵌套语句,错误的递归,非法计算,可能出现的空指针引用等等。统计证明,在整个软件开发生命周期中,30%至70%的代码逻辑设计和编码缺陷是可以通过静态代码分析来发现和修复的。在C++项目开发过程中,因为其为编译执行语言,语言规则要求较高,开发团队往往要花费大量的时间和精力发现并修改代码缺陷。所以C++静态代码分析工具能够帮助开发人员快速、有效的定位代码缺陷并及时纠正这些问题,从而极大地提高软件可靠性并节省开发成本。 静态代码分析工具的优势: 1、自动执行静态代码分析,快速定位代码隐藏错误和缺陷。 2、帮助代码设计人员更专注于分析和解决代码设计缺陷。 3、减少在代码人工检查上花费的时间,提高软件可靠性并节省开发成本。 一些主流的静态代码检测工具,免费的cppcheck,clang static analyzer; 商用的coverity,pclint等 各个工具性能对比: http://www./html/19/n-3709719.html
所谓的代码动态检测,就是需要再程序运行情况下,通过插入特殊指令,进行动态检测和收集运行数据信息,然后分析给出报告。 1.为了检测内存非法使用,需要hook内存分配和操作函数。hook的方法可以是用C-preprocessor,也可以是在链接库中直接定义(因为Glibc中的malloc/free等函数都是weak symbol),或是用LD_PRELOAD。另外,通过hook strcpy(),memmove()等函数可以检测它们是否引起buffer overflow。 工具总结对比,常用valgrind(检测内存泄露),gperftools(统计内存消耗)等:
BI: dynamic binary instrumentation https://github.com/google/sanitizers/wiki/AddressSanitizerComparisonOfMemoryTools 2. C++内存管理效率问题
自底向上分别是:
当然应用程序也可以直接使用系统调用从内核分配内存,自己根据程序特性来维护内存,但是会大大增加开发成本。 2. C++内存管理问题
sbrk/brk系统调用的实现:分配内存是通过调节堆顶的位置来实现, 堆顶的位置是通过函数 brk 和 sbrk 进行动态调整,参考例子: (1) 初始状态:如图 (1) 所示,系统已分配 ABCD 四块内存,其中 ABD 在堆内分配, C 使用 mmap 分配。为简单起见,图中忽略了如共享库等文件映射区域的地址空间。 (2) E=malloc(100k) :分配 100k 内存,小于 128k ,从堆内分配,堆内剩余空间不足,扩展堆顶 (brk) 指针。 (3) free(A) :释放 A 的内存,在 glibc 中,仅仅是标记为可用,形成一个内存空洞 ( 碎片 ),并没有真正释放。如果此时需要分配 40k 以内的空间,可重用此空间,剩余空间形成新的小碎片。 (4) free(C) :C 空间大于 128K ,使用 mmap 分配,如果释放 C ,会调用 munmap 系统调用来释放,并会真正释放该空间,还给 OS ,如图 (4) 所示。
所以free的内存不一定真正的归还给OS,随着系统频繁地 malloc 和 free ,尤其对于小块内存,堆内将产生越来越多不可用的碎片,导致“内存泄露”。而这种“泄露”现象使用 valgrind 是无法检测出来的。
3. 常用解决上述问题的方案内存池技术 内存池方案通常一次从系统申请一大块内存块,然后基于在这块内存块可以进行不同内存策略实现,可以比较好得解决上面提到的问题,一般采用内存池有以下好处: 1.少量系统申请次数,非常少(几没有) 堆碎片。 6.减少额外系统内存管理开销,可以节约内存; 内存管理方案实现的指标:
各个内存分配器的实现都是在以上的各种指标中进行权衡选择. 4. 一些业界主流的内存管理方案SGI STL allocator 是比较优秀的 C++库内存分配器(细节参考上面描述) ptmalloc是glibc的内存分配管理模块, 主要核心技术点:
ptmalloc的缺陷
tcmallocgoogle的gperftools内存分配管理模块, 主要核心技术点:
TCMalloc给每个线程分配了一个线程局部缓存。小分配可以直接由线程局部缓存来满足。需要的话,会将对象从中央数据结构移动到线程局部缓存中,同时定期的垃圾收集将用于把内存从线程局部缓存迁移回中央数据结构中:
2. Thread Specific Free List/size-classes [8,16,32,…32k]: 更好小对象内存分配; 每个小对象的大小都会被映射到170个可分配的尺寸类别中的一个。例如,在分配961到1024字节时,都会归整为1024字节。尺寸类别这样隔开:较小的尺寸相差8字节,较大的尺寸相差16字节,再大一点的尺寸差32字节,如此类推。最大的间隔(对于尺寸 >= ~2K的)是256字节。一个线程缓存对每个尺寸类都包含了一个自由对象的单向链表
3. The central page heap:更好的大对象内存分配,一个大对象的尺寸(> 32K)会被除以一个页面尺寸(4K)并取整(大于结果的最小整数),同时是由中央页面堆来处理 的。中央页面堆又是一个自由列表的阵列。对于
4. Spans: TCMalloc管理的堆由一系列页面组成。连续的页面由一个“跨度”( 由页面号索引的中央数组可以用于找到某个页面所属的跨度。例如,下面的跨度a占据了2个页面,跨度b占据了1个页面,跨度c占据了5个页面最后跨度d占据了3个页面。 tcmalloc的改进
jemallocFreeBSD的提供的内存分配管理模块, 主要核心技术点: 1. 与tcmalloc类似,每个线程同样在<32KB的时候无锁使用线程本地cache; 2. Jemalloc在64bits系统上使用下面的size-class分类: 3. small/large对象查找metadata需要常量时间, huge对象通过全局红黑树在对数时间内查找 4. 虚拟内存被逻辑上分割成chunks(默认是4MB,1024个4k页),应用线程通过round-robin算法在第一次malloc的时候分配arena, 每个arena都是相互独立的,维护自己的chunks, chunk切割pages到small/large对象。free()的内存总是返回到所属的arena中,而不管是哪个线程调用free().
上图可以看到每个arena管理的arena chunk结构, 开始的header主要是维护了一个page map(1024个页面关联的对象状态), header下方就是它的页面空间。Small对象被分到一起, metadata信息存放在起始位置。large chunk相互独立,它的metadata信息存放在chunk header map中。 5. 通过arena分配的时候需要对arena bin(每个small size-class一个,细粒度)加锁,或arena本身加锁。并且线程cache对象也会通过垃圾回收指数退让算法返回到arena中。 jemalloc的优化
性能比较 测试环境:2x Intel E5/2.2Ghz with 8 real cores per socket,16 real cores, 开启hyper-threading, 总共32个vcpu。16个table,每个5M row。OLTP_RO测试包含5个select查询:select_ranges, select_order_ranges, select_distinct_ranges, select_sum_ranges: facebook的测试结果: 服务器吞吐量分别用6个malloc实现的对比数据,可以看到tcmalloc和jemalloc最好(tcmalloc这里版本较旧)。 详细参考: https://www./notes/facebook-engineering/scalable-memory-allocation-using-jemalloc/480222803919 总结 可以看出tcmalloc和jemalloc性能接近,比ptmalloc性能要好,在多线程环境使用tcmalloc和jemalloc效果非常明显。一般支持多核多线程扩展情况下可以使用jemalloc;反之使用tcmalloc可能是更好的选择。 可以参考: https://sploitfun./2015/02/10/understanding-glibc-malloc/comment-page-1/ http://goog-perftools./doc/tcmalloc.html https://www./notes/facebook-engineering/scalable-memory-allocation-using-jemalloc/480222803919 https://blog.csdn.net/junlon2006/article/details/77854898 思考问题: 1 jemalloc和tcmalloc最佳实践是什么? 2 内心池的设计有哪些套路?为什么? 七 C++程序内存性能测试
通过读取/proc/$PID/maps 和 smaps 的数据,解析数据,生成进程的虚列内存映像和一些内存统计:
里面可以查看程序堆和栈内存大小区间,程序所占内存大小,主要是关注PSS 以下内存统计名称解释: VSS:Virtual Set Size,虚拟内存耗用内存,包括共享库的内存; RSS:Resident Set Size,实际使用物理内存,包括共享库; PSS:Proportional Set Size,实际使用的物理内存,共享库按比例分配; USS:Unique Set Size,进程独占的物理内存,不计算共享库,也可以理解为将进程杀 死能释放出的内存; 一般VSS >= RSS >= PSS >= USS, 一般统计程序的内存占用,PSS是最好的选择,比较合理。
实时显示内存当前使用情况和各个进程使用内存信息
查看系统可用内存和占用情况
查看机器使用内存使用统计和内存硬件基本信息。
监控内存变化详细请参考man手册: http://linuxtools-rst./zh_CN/latest/tool/ 思考问题: 1 各个工具优缺点和使用场景? 2 linux内存统计里面,划分了哪些统计? 参加答案 2. valgrind massif堆栈分析器,指示程序中使用了多少堆内存等信息,可以帮助你减少程序内存使用量,因为更小程序更能多占cache,减少分页,加速程序;对于需要大量内存的程序,可以让程序能够减少交换分区使用,加速程序。valgrind massif 采集完数据生成数据文件,数据文件会显示每一帧的程序使用的堆内存大小, The Snapshot Details 显示更多细节: 更多细节参考: http:///docs/manual/ms-manual.html 3. gperftools--heap profilegperftools工具里面的内存监控器,统计监控程序使用内存的多少,可以查看内存使用热点,默认是100ms一次采样。 text模式:% pprof --text test_tc test.prof Total: 38 samples
基本上只要知道这些,就能很好的掌握每一时刻程序运行内存使用情况了,并且对比不同时段的不同profile数据,可以分析出内存走向,进而定位热点和泄漏。 pdf模式:可以把采样的结果转换为图模式,这样查看更为直观:
Kcachegrind模式:利用pprof生成callgrind格式的文件即可,KCachegrind的GUI工具,用于分析
更多细节参考 https://github.com/gperftools/gperftools/blob/master/docs/heapprofile.html windows 版本: https:///projects/precompiledbin/files/latest/download?source=files 思考问题: 1 说一说内存对设备(手机,PC,嵌入式设备)性能影响? 参考: https://blog.csdn.net/yang_yulei/article/details/45795591 https://blog.csdn.net/buxizhizhou530/article/details/46695999 http://www.cnblogs.com/heleifz/p/shared-principle-application.html https:///gotw/_102/ https://lanzkron./2012/04/22/make_shared-almost-a-silver-bullet/ http://en./w/cpp/memory/shared_ptr/make_shared
|
|
来自: 西北望msm66g9f > 《华为》