亚洲欧美第一页_禁久久精品乱码_粉嫩av一区二区三区免费野_久草精品视频

蟲蟲首頁| 資源下載| 資源專輯| 精品軟件
登錄| 注冊

您現在的位置是:首頁 > 技術閱讀 >  超硬核文章 | C++內存管理全景指南

超硬核文章 | C++內存管理全景指南

時間:2024-02-13


喵哥技術交流群發現了很多水平很高的朋友,歡迎大家來加喵哥微信,進群一起討論計算機知識!

程序喵大人微信

導語 深入理解C++內存管理,一文了解所有C++內存問題,萬字長文,建議收藏   


隨著人工智能,云計算等技術的迅猛發展,讓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 -aLinux alexfeng 3.19.0-15-generic #15-Ubuntu SMP Thu Apr 16 23:32:37 UTC 2015 x86_64 x86_64 x86_64 GNU/Linuxcat /proc/cpuinfobugs            :bogomips        : 4800.52clflush size    : 64cache_alignment : 64address sizes   : 36 bits physical, 48 bits virtualcat /proc/meminfoMemTotal:        4041548 kB(4G)MemFree:          216304 kBMemAvailable:    2870340 kBBuffers:          983360 kBCached:          1184008 kBSwapCached:        54528 kBGNU gdb (Ubuntu 7.9-1ubuntu1) 7.9g++ (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++ 程序的內存是一或多個相接的字節序列。內存中的每個字節擁有唯一的地址。


字節


字節是最小的可尋址內存單元。它被定義為相接的位序列,大到足以保有任何 UTF-8 編碼單元( 256 個相異值)和 (C++14 起)基本執行字符集(要求為單字節的 96 個字符)的任何成員。類似 C , C++ 支持 8 位或更大的字節。char 、 unsigned char 和 signed char 類型把一個字節用于存儲和值表示。字節中的位數可作為 CHAR_BIT 或 std::numeric_limits<unsigned char>::digits 訪問。


內存位置


內存位置是

  • 一個標量類型(算術類型、指針類型、枚舉類型或 std::nullptr_t )對象

  • 或非零長位域的最大相接序列

注意:各種語言特性,例如引用和虛函數,可能涉及到程序不可訪問,但為實現所管理的額外內存位置。


線程與數據競爭


  • 執行線程是程序中的控制流,它始于 std::thread::thread 、 std::async 或以其他方式所做的頂層函數調用。

  • 任何線程都能潛在地訪問程序中的任何對象(擁有自動或線程局域存儲期的對象仍可為另一線程通過指針或引用訪問)。

  • 始終允許不同的執行線程同時訪問(讀和寫)不同的內存位置,而無沖突或同步要求。


一個表達式的求值寫入內存位置,而另一求值讀或寫同一內存位置時,稱這些表達式沖突。擁有二個沖突求值的程序有數據競爭,除非

  • 兩個求值都在同一線程上,或同一信號處理函數中執行,或

  • 兩個沖突求值都是原子操作(見 std::atomic ),或

  • 一個沖突求值先發生于( happens-before )另一個(見內存順序--std::memory_order )

若出現數據競爭,則程序的行為未定義。


內存順序(std::memory_order)

如果不使用任何同步機制(例如 mutex 或 atomic),在多線程中讀寫同一個變量,那么程序的結果是難以預料的。簡單來說,編譯器以及 CPU 的一些行為,會影響到C++程序的執行結果

  • 即使是簡單的語句,C++ 也不保證是原子操作。

  • CPU 可能會調整指令的執行順序。

  • 在 CPU cache 的影響下,一個 CPU 執行了某個指令,不會立即被其它 CPU 看見。

  • Intel x86, x86-64等屬于強排序CPU,x86-64的強內存模型總能保證按順序執行,遵從數據依賴順序,但PowerPC和ARM是弱排序CPU,有時需要依賴內存柵欄指令。

多線程讀寫同一變量需要使用同步機制,最常見的同步機制就是std::mutexstd::atomic。然而從性能角度看,通常使用std::atomic會獲得更好的性能. 


C++11 提供6 種可以應用于原子變量的內存次序:


  • momory_order_relaxed,

  • memory_order_consume,

  • memory_order_acquire,

  • memory_order_release,

  • memory_order_acq_rel,

  • memory_order_seq_cst


雖然共有 6 個選項,但它們表示的是四種內存模型:


  • Relaxed ordering

  • Release-Acquire ordering

  • Release-Consume ordering

  • Sequentially-consistent ordering


順序一致次序(sequential consisten ordering)


對應memory_order_seq_cst. SC作為默認的內存序,是因為它意味著將程序看做是一個簡單的序列。如果對于一個原子變量的操作都是順序一致的,那么多線程程序的行為就像是這些操作都以一種特定順序被單線程程序執行。從同的角度來看,一個順序一致的 store 操作 synchroniezd-with 一個順序一致的需要讀取相同的變量的 load 操作。除此以外,順序模型還保證了在 load 之后執行的順序一致原子操作都得表現得在 store 之后完成。非順序一致內存次序(non-sequentially consistency memory ordering)強調對同一事件(代碼),不同線程可以以不同順序去執行,不僅是因為編譯器可以進行指令重排,也因為不同的 CPU cache 及內部緩存的狀態可以影響這些指令的執行。但所有線程仍需要對某個變量的連續修改達成順序一致。


松弛次序(relaxed ordering)


在這種模型下,std::atomicload()store()都要帶上memory_order_relaxed參數。Relaxed ordering 僅僅保證load()store()是原子操作,除此之外,不提供任何跨線程的同步。


獲取-釋放次序(acquire-release ordering)


在這種模型下,store()使用memory_order_release,而load()使用memory_order_acquire。這種模型有兩種效果,第一種是可以限制 CPU 指令的重排:
  • store()之前的所有讀寫操作,不允許被移動到這個store()的后面。

  • load()之后的所有讀寫操作,不允許被移動到這個load()的前面。

 


數據依賴(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,消費和獲取都為了同一個目的:幫助非原子信息在線程間安全的傳遞。就像獲取操作一樣,消費操作必須與另一個線程的釋放操作一起使用。它們之間主要的區別在于消費操作可以正確起作用的案例更少。相對于它的使用不便,反過來也就意味著消費操作在某些平臺使用更有效。



默認情況下,std::atomic使用的是 Sequentially-consistent ordering。但在某些場景下,合理使用其它三種 ordering,可以讓編譯器優化生成的代碼,從而提高性能。


思考問題:

1  C++正常程序可以訪問到哪些內存和不能訪問到哪些內存(這些內存屬于該程序)?

2  內存對程序并發執行有什么影響?

3  std::memory_order 的作用是什么?

 


二  C++對象內存模型


1 空類對象(一般作為模板的tag來使用)

class A { };
sizeof(A) = 1
C++標準要求C++的對象大小不能為0,C++對象必須在內存里面有唯一的地址,
但又不想浪費太多內存空間,所以標準規定為1byte,


2  非空類

class A{public   int a;};sizeof(A ) = 8  ,align=8



3  非空虛基類

class A{public   int a;   virtual void v();};sizeof(A ) = 16  ,align=8

                                                         

4 單繼承

class A {public:  int a;  virtual void v();};class B : public A {public:  int b;};sizeof(B) = 16, align = 8


                  

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

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;  void w();};sizeof(C) = 32 ,align = 8


7 The Diamond: 多重繼承 (沒有虛繼承)

class A {public:  int a;  virtual void v();};class B : public A {public:  int b;  virtual void w();};class C : public A {public:  int c;  virtual void x();};class D : public B, public C {public:  int d;  virtual void y();};sizeof(D)  = 40 align = 8
 

                      

注意點:此種繼承存在兩份基類成員,使用時候需要指定路徑,不方便,易出錯。



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.kernel.org/doc/Documentation/x86/x86_64/mm.txt

剩下的是用戶內存空間:

  • stack棧區:專門用來實現函數調用-棧結構的內存塊。相對空間下(可以設置大小,Linux 一般默認是8M,可通過 ulimit –s 查看),系統自動管理,從高地址往低地址,向下生長。

  • 內存映射區:包括文件映射和匿名內存映射, 應用程序的所依賴的動態庫,會在程序執行時候,加載到內存這個區域,一般包括數據(data)和代碼(text);通過mmap系統調用,可以把特定的文件映射到內存中,然后在相應的內存區域中操作字節來訪問文件內容,實現更高效的IO操作;匿名映射,在glibc中malloc分配大內存的時候會用到匿名映射。這里所謂的“大”表示是超過了MMAP_THRESHOLD 設置的字節數,它的缺省值是 128 kB,可以通過 mallopt() 去調整這個設置值。還可以用于進程間通信IPC(共享內存)。

  • heap堆區:主要用于用戶動態內存分配,空間大,使用靈活,但需要用戶自己管理,通過brk系統調用控制堆的生長,向高地址生長。

  • BBS段和DATA段:用于存放程序全局數據和靜態數據,一般未初始化的放在BSS段(統一初始化為0,不占程序文件的空間),初始化的放在data段,只讀數據放在rodata段(常量存儲區)。

  • text段:主要存放程序二進制代碼。


2  為了防止內存被攻擊,比如棧溢出攻擊和堆溢出攻擊等,Linux在特定段之間使用隨機偏移,使段的起始地址是隨機值, Linux 系統上的ASLR 等級可以通過文件 /proc/sys/kernel/randomize_va_space 來進行設置,它支持以下取值:

  • 0 - 關閉的隨機化。一切都是靜止的。

  • 1 - 保守的隨機化。共享庫、棧、mmap()、VDSO以及堆將被隨機化。

  • 2 - 完全的隨機化。除了上面列舉的要素外,通過 brk() 分配得到的內存空間也將被隨機化。


3  每個段都有特定的安全控制(權限):

vm_flags

第三列,如r-xp

此段虛擬地址空間的屬性。每種屬性用一個字段表示,r表示可讀,w表示可寫,x表示可執行,p和s共用一個字段,互斥關系,p表示私有段,s表示共享段,如果沒有相應權限,則用’-’代替

4 Linux虛擬內存是按頁分配,每頁大小為4KB或者2M,1G等(大頁內存), 默認是4K;


5 例子-通過pmap 查看程序內存布局(綜合proc/x/maps與proc/x/smaps數據):

#include<iostream>#include <unistd.h>using namespace std;//long  a[1024*1024] = {0};int main(){    void *heap;    int *x = new int[1024]();    cout << hex <<"x: " << x <<endl;    heap = sbrk(0);    //cout << hex << "a:" << (long) &a <<endl;    cout << hex << "heap: " << (long) heap <<endl;    cout << hex << "heap: " << (long)heap - (long)x <<endl;    while(1);    return 0; }
 g++  -g  -std=c++11 -o main  mem.cpp./main

關閉內存地址隨機化

pmap -X 81178117:   ./main          Address Perm   Offset Device    Inode  Size  Rss Pss Referenced Anonymous Swap Locked Mapping     00400000 r-xp 00000000  08:11 43014235     4    4   4          4         0    0      0 main     00601000 r--p 00001000  08:11 43014235     4    4   4          4         4    0      0 main     00602000 rw-p 00002000  08:11 43014235     4    4   4          4         4    0      0 main    //程序的text段,只讀數據段,和全局/靜態數據段;
00603000 rw-p 00000000 00:00 0 136 8 8 8 8 0 0 [heap] //程序的堆內存段;
7ffff71e2000 r-xp 00000000 08:11 266401 88 88 18 88 0 0 0 libgcc_s.so.1 7ffff71f8000 ---p 00016000 08:11 266401 2044 0 0 0 0 0 0 libgcc_s.so.1 7ffff73f7000 rw-p 00015000 08:11 266401 4 4 4 4 4 0 0 libgcc_s.so.1 7ffff73f8000 r-xp 00000000 08:11 266431 1052 224 3 224 0 0 0 libm-2.21.so 7ffff74ff000 ---p 00107000 08:11 266431 2044 0 0 0 0 0 0 libm-2.21.so 7ffff76fe000 r--p 00106000 08:11 266431 4 4 4 4 4 0 0 libm-2.21.so 7ffff76ff000 rw-p 00107000 08:11 266431 4 4 4 4 4 0 0 libm-2.21.so 7ffff7700000 r-xp 00000000 08:11 266372 1792 1152 8 1152 0 0 0 libc-2.21.so 7ffff78c0000 ---p 001c0000 08:11 266372 2048 0 0 0 0 0 0 libc-2.21.so 7ffff7ac0000 r--p 001c0000 08:11 266372 16 16 16 16 16 0 0 libc-2.21.so 7ffff7ac4000 rw-p 001c4000 08:11 266372 8 8 8 8 8 0 0 libc-2.21.so 7ffff7ac6000 rw-p 00000000 00:00 0 16 12 12 12 12 0 0 7ffff7aca000 r-xp 00000000 08:11 46146360 960 856 283 856 0 0 0 libstdc++.so.6.0.20 7ffff7bba000 ---p 000f0000 08:11 46146360 2048 0 0 0 0 0 0 libstdc++.so.6.0.20 7ffff7dba000 r--p 000f0000 08:11 46146360 32 32 32 32 32 0 0 libstdc++.so.6.0.20 7ffff7dc2000 rw-p 000f8000 08:11 46146360 8 8 8 8 8 0 0 libstdc++.so.6.0.20 7ffff7dc4000 rw-p 00000000 00:00 0 84 16 16 16 16 0 0 7ffff7dd9000 r-xp 00000000 08:11 266344 144 144 1 144 0 0 0 ld-2.21.so//程序的內存映射區,主要是動態庫加載到該內存區,包括動態庫的text代碼段和數據data段。//中間沒有名字的,屬于程序的匿名映射段,主要提供大內存分配。 7ffff7fd4000 rw-p 00000000 00:00 0 20 20 20 20 20 0 0 7ffff7ff5000 rw-p 00000000 00:00 0 12 12 12 12 12 0 0 7ffff7ff8000 r--p 00000000 00:00 0 8 0 0 0 0 0 0 [vvar] 7ffff7ffa000 r-xp 00000000 00:00 0 8 4 0 4 0 0 0 [vdso]//vvar page,kernel的一些系統調用的數據會映射到這個頁面,用戶可以直接在用戶空間訪問;//vDSO -virtual dynamic shared object,is a small shared library exported by the kernel to accelerate the execution of certain system calls that do not necessarily have to run in kernel space, 就是內核實現了glibc的一些系統調用,然后可以直接在用戶空間執行,提高系統調用效率和減少與glibc的耦合。7ffff7ffc000 r--p 00023000 08:11 266344 4 4 4 4 4 0 0 ld-2.21.so7ffff7ffd000 rw-p 00024000 08:11 266344 4 4 4 4 4 0 0 ld-2.21.so7ffff7ffe000 rw-p 00000000 00:00 0 4 4 4 4 4 0 07ffffffde000 rw-p 00000000 00:00 0 136 8 8 8 8 0 0 [stack]//此段為程序的棧區
ffffffffff600000 r-xp 00000000 00:00 0 4 0 0 0 0 0 0 [vsyscall]//此段是Linux實現vsyscall系統調用vsyscall庫代碼段 ===== ==== === ========== ========= ==== ====== 12744 2644 489 2644 172 0 0 KB


思考問題:

1  棧為什么要由高地址向低地址擴展,堆為什么由低地址向高地址擴展?

  • 歷史原因:在沒有MMU的時代,為了最大的利用內存空間,堆和棧被設計為從兩端相向生長。那么哪一個向上,哪一個向下呢?人們對數據訪問是習慣于向上的,比如你在堆中new一個數組,是習慣于把低元素放到低地址,把高位放到高地址,所以堆 向上生長比較符合習慣,  而棧則對方向不敏感,一般對棧的操作只有PUSH和pop,無所謂向上向下,所以就把堆放在了低端,把棧放在了高端. 但現在已經習慣這樣了。這個和處理器設計有關系,目前大多數主流處理器都是這樣設計,但ARM 同時支持這兩種增長方式。

2  如何查看進程虛擬地址空間的使用情況?

3  對比堆和棧優缺點?

 

四  C++棧內存空間模型


  1. C++程序運行調用棧示意圖:

  函數調用過程中,棧(有俗稱堆棧)的變化:

      from  https://zhuanlan.zhihu.com/p/25816426

  1. 當主函數調用子函數的時候:
  • 在主函數中,將子函數的參數按照一定調用約定(參考調用約定),一般是從右向左把參數push到棧中;

  • 然后把下一條指令地址,即返回地址(return address)push入棧(隱藏在call指令中);

  • 然后跳轉到子函數地址處執行:call 子函數;此時

   2.  子函數執行:
  • push  %rbp  :  把當前rbp的值保持在棧中;

  • mov %rsp, %rbp:把rbp移到最新棧頂位置,即開啟子函數的新幀;

  • [可選]sub $xxx, %esp:  在棧上分配XXX字節的臨時空間。(抬高棧頂)(編譯器根據函數中的局部變量的總大小確定臨時空間的大小);

  • [可選]push XXX:    保存(push)一些寄存器的值;

    3.  子函數調用返回:
  • 保持返回值:一般將函數函數值保持在eax寄存器中;

  • [可選]恢復(pop)一些寄存器的值;

  • mov %rbp,%rsp: 收回棧空間,恢復主函數的棧頂;

  • pop %rbp;恢復主函數的棧底;

    在AT&T中:

    以上兩條指令可以被leave指令取代

  • leave

  • ret;從棧頂獲取之前保持的返回地址(return address),并跳轉到此位置執行;

棧攻擊

由上面棧內存布局可以看出,棧很容易被破壞和攻擊,通過棧緩沖器溢出攻擊,用攻擊代碼首地址來替換函數幀的返回地址,當子函數返回時,便跳轉到攻擊代碼處執行,獲取系統的控制權,所以操作系統和編譯器采用了一些常用的防攻擊的方法:

  • ASLR(地址空間布局隨機化):操作系統可以將函數調用棧的起始地址設為隨機化(這種技術被稱為內存布局隨機化,即Address Space Layout Randomization (ASLR) ),加大了查找函數地址及返回地址的難度。

  • Cannary 

  gcc關于棧溢出檢測的幾個參數:

     開啟Canary之后,函數開始時在ebp和臨時變量之間插入一個隨機值,函數結束時驗證這個值。如果不相等(也就是這個值被其他值覆蓋了),就會調用 _stackchk_fail函數,終止進程。對應GCC編譯選項-fno-stack-protector解除該保護。

  • NX.
    開啟NX保護之后,程序的堆棧將會不可執行。對應GCC編譯選項
    -z execstack解除該保護。


棧異常處理


  • 一個函數(或方法)拋出異常,那么它首先將當前棧上的變量全部清空(unwinding),如果變量是類對象的話,將調用其析構函數,接著,異常來到call stack的上一層,做相同操作,直到遇到catch語句。

  • 指針是一個普通的變量,不是類對象,所以在清空call stack時,指針指向資源的析構函數將不會調用。


思考問題:

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++默認的調用約定
2. stdcall

它是微軟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:

  • Global: All three versions of operator new are declared in the global namespace, not within thestdnamespace.

  • Implicit: The allocating versions ((1) and (2)) are implicitly declared in every translation unit of a C++ program, no matter whether header <new> is included or not.

  • Replaceable: The allocating versions ((1) and (2)) are also replaceable: A program may provide its own definition that replaces the one provided by default to produce the result described above, or can overload it for specific types.

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.

operator new can be called explicitly as a regular function, but in C++, new is an operator with a very specific behavior: An expression with the new operator, first calls function operator new (i.e., this function) with the size of its type specifier as first argument, and if this is successful, it then automatically initializes or constructs the object (if needed). Finally, the expression evaluates as a pointer to the appropriate type.

from http://www.cplusplus.com/reference/new/operator%20new/


1.是用來專門分配內存的函數,為new操作符調用,你能增加額外的參數重載函數operator new(有限制):

        限制1:第一個參數類型必須是size_t;

        限制2:函數必須返回void*;

2.operator new 底層一般調用malloc函數(gcc+glibc)分配內存;

3.operator new 分配失敗會拋異常(默認),通過傳遞參數也可以不拋異常,返回空指針;


operator delete :

1.是用來專門分配內存的函數,為delete操作符調用,你能增加額外的參數重載函數operator delete(有限制):

       限制1:第一個參數類型必須是void*;

       限制2:函數必須返回void;

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[] 表達式
  • 對應會調用operator new[]/delete[]函數;

  • 按對象的個數,分別調用構造函數和析構函數;


http://www.cplusplus.com/reference/new/operator%20new[]/


class-specific allocation functions(成員函數)


http://en.cppreference.com/w/cpp/memory/new/operator_new


定制對象特殊new/delete函數;


 實現一般是使用全局:


::operator new

::operator delete


關鍵點:

  • 你想在堆上建立一個對象,應該用new操作符。它既分配內存又為對象調用構造函數。

  • 如果你僅僅想分配內存,就應該調用operator new函數;它不會調用構造函數。

  • 如果你想定制自己的在堆對象被建立時的內存分配過程,你應該寫你自己的operator new函數,然后使用new操作符,new操作符會調用你定制的operator new。

  • 如果你想在一塊已經獲得指針的內存里建立一個對象,應該用placement new。

  • C++可以為分配失敗設置自己的異常處理函數:

  If set_new_handler  has been used to define a new_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.

  • 如果在構造函數時候拋出異常,new表達式后面會調用對應operator delete函數釋放內存:

The other signatures ((2) and (3)) are never called by a delete-expression (the delete operator always calls the ordinary version of this function, and exactly once for each of its arguments). These other signatures are only called automatically by a new-expression when their object construction fails (e.g., if the constructor of an object throws while being constructed by a new-expressionwithnothrow, the matchingoperator deletefunction accepting anothrowargument is called).

 

思考問題:

1  malloc和free是怎么實現的?

2  malloc 分配多大的內存,就占用多大的物理內存空間嗎?

3  free 的內存真的釋放了嗎(還給 OS ) ?

4  既然堆內內存不能直接釋放,為什么不全部使用 mmap 來分配?

5  如何查看堆內內存的碎片情況?

6  除了 glibc 的 malloc/free ,還有其他第三方實現嗎?

 


2.  C++11的智能指針與垃圾回收


  • C++智能指針出現是為了解決由于支持動態內存分配而導致的一些C++內存問題,比如內存泄漏,對象生命周期的管理,懸掛指針(dangling pointer)/空指針等問題;

  • C++智能指針通過RAII設計模式去管理對象生命周期(動態內存管理),提供帶少量異常類似普通指針的操作接口,在對象構造的時候分配內存,在對象作用域之外釋放內存,幫助程序員管理動態內存;

  • 老的智能指針auto_ptr由于設計語義不好而導致很多不合理問題:不支持復制(拷貝構造函數)和賦值(operator =),但復制或賦值的時候不會提示出錯。因為不能被復制,所以不能被放入容器中。而被C++11棄用(deprecated);



新的智能指針:


    1. shared_ptr


  • shared_ptr是引用計數型(reference counting)智能指針, shared_ptr包含兩個成員,一個是指向真正數據的指針,另一個是引用計數ref_count模塊指針,對比GCC實現,大致原理如下,

               

共享對象(數據)(賦值拷貝),引用計數加1,指針消亡,引用計數減1,當引用計數為0,自動析構所指的對象,引用計數是線程安全的(原子操作)。


shared_ptr關鍵點:

  • 用shared_ptr就不要new,保證內存管理的一致性;

  • 使用weak_ptr來打破循環引用;

  • 用make_shared來生成shared_ptr,提高效率,內存分配一次搞定,防止異常導致內存泄漏,參考https://herbsutter.com/gotw/_102/;

  • 大量的shared_ptr會導致程序性能下降(相對其他指針),需要等到所有的weak引用為0時才能最終釋放內存(delete);

  • 用enable_shared_from_this來使一個類能獲取自身的shared_ptr;

  • 不能在對象的構造函數中使用shared_from_this()函數,因為對象還沒有構造完畢,share_ptr還沒有初始化構造完全;構造順序:先需要調用enable_shared_from_this類的構造函數,接著調用對象的構造函數,最后需要調用shared_ptr類的構造函數初始化enable_shared_from_this的成員變量weak_this_。然后才能使用shared_from_this()函數;               

 

2. unique_ptr


獨占指針,不共享,不能賦值拷貝;


unique_ptr關鍵點:

  1. 如果對象不需要共享,一般最好都用unique_ptr,性能好,更安全;

  2. 可以通過move語義傳遞對象的生命周期控制權;

  3. 函數可以返回unique_ptr對象,為什么? 

 RVO和NRVO

       當函數返回一個對象時,理論上會產生臨時變量,那必然是會導致新對象的構造和舊對象的析構,這對效率是有影響的。C++編譯針對這種情況允許進行優化,哪怕是構造函數有副作用,這叫做返回值優化(RVO),返回有名字的對象叫做具名返回值優化(NRVO),就那RVO來說吧,本來是在返回時要生成臨時對象的,現在構造返回對象時直接在接受返回對象的空間中構造了。假設不進行返回值優化,那么上面返回unique_ptr會不會有問題呢?也不會。因為標準允許編譯器這么做:

1.如果支持move構造,那么調用move構造。

2.如果不支持move,那就調用copy構造。

3.如果不支持copy,那就報錯吧。

顯然的,unique_ptr是支持move構造的,unique_ptr對象可以被函數返回。

 

 3. weak_ptr


  • 引用對象,不增加引用計數,對象生命周期,無法干預;

  • 配合shared_ptr解決shared_ptr循環引用問題;

  • 可以影響到對象內存最終釋放的時間;


更詳細參考:

http://en.cppreference.com/w/cpp/memory/shared_ptr


思考問題:

1 C++的賦值和Java的有什么區別?

 C++的賦值可以是對象拷貝也可以對象引用,java的賦值是對象引用;

2 smart_ptr有哪些坑可以仍然導致內存泄漏?

 2.1.shared_ptr初始化構造函數指針,一般是可以動態管理的內存地址,如果不是就可能導致內存泄漏;

 2.2.shared_ptr要求內部new和delete實現必須是成對,一致性,如果不是就可能導致內存泄漏;

 2.3. shared_ptr對象和其他大多數STL容器一樣,本身不是線程安全的,需要用戶去保證;

3 unique_ptr有哪些限制?

  • 只能移動賦值轉移數據,不能拷貝;

  • 不支持類型轉換(cast);

4 智能指針是異常安全的嗎?

 

所謂異常安全是指,當異常拋出時,帶有異常安全的函數會:

  •     不泄露任何資源

  •     不允許數據被破壞


智能指針就是采用RAII技術,即以對象管理資源來防止資源泄漏。

Exception Safety

Several functions in these smart pointer classes are specified as having "no effect" or "no effect except such-and-such" if an exception is thrown. This means that when an exception is thrown by an object of one of these classes, the entire program state remains the same as it was prior to the function call which resulted in the exception being thrown. This amounts to a guarantee that there are no detectable side effects. Other functions never throw exceptions. The only exception ever thrown by functions which do throw (assuming T meets the  common requirements) is std::bad_alloc, and that is thrown only by functions which are explicitly documented as possibly throwing std::bad_alloc. 

https://www.boost.org/doc/libs/1_61_0/libs/smart_ptr/smart_ptr.htm


5 智能指針是線程安全的嗎?


智能指針對象的引用計數模塊是線程安全的,因為 shared_ptr 有兩個數據成員,讀寫操作不能原子化,所以對象本身不是線程安全的,需要用戶去保證線程安全。

Thread Safety

shared_ptr objects offer the same level of thread safety as built-in types. A shared_ptr instance can be "read" (accessed using only const operations) simultaneously by multiple threads. Different shared_ptr instances can be "written to" (accessed using mutable operations such as operator= or reset) simultaneously by multiple threads (even when these instances are copies, and share the same reference count underneath.)

Any other simultaneous accesses result in undefined behavior.

https://www.boost.org/doc/libs/1_67_0/libs/smart_ptr/doc/html/smart_ptr.html#shared_ptr_thread_safety


C++標準垃圾回收


C++11 提供最小垃圾支持

declare_reachable
undeclare_reachable
declare_no_pointers
undeclare_no_pointers
pointer_safety
get_pointer_safety

由于很多場景受限,當前幾乎沒有人使用;

感興趣可以參考: 

http://www.stroustrup.com/C++11FAQ.html#gc-abi

http://www.openstd.org/jtc1/sc22/wg21/docs/papers/2008/n2585.pdf


思考問題:

1  C++可以通過哪些技術來支持“垃圾回收”?

    smart_ptr,RAII, move語義等;

2  RAII是指什么?

RAII是指Resource Acquisition IInitialization的設計模式,

RAII要求,資源的有效期與持有資源的對象的生命期嚴格綁定,即由對象的構造函數完成資源的分配(獲取),同時由析構函數完成資源的釋放。在這種要求下,只要對象能正確地析構,就不會出現資源泄露問題。)

當一個函數需要通過多個局部變量來管理資源時,RAII就顯得非常好用。因為只有被構造成功(構造函數沒有拋出異常)的對象才會在返回時調用析構函數,同時析構函數的調用順序恰好是它們構造順序的反序,這樣既可以保證多個資源(對象)的正確釋放,又能滿足多個資源之間的依賴關系。

由于RAII可以極大地簡化資源管理,并有效地保證程序的正確和代碼的簡潔,所以通常會強烈建議在C++中使用它。

from https://zh.wikipedia.org/wiki/RAII

3.  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:寫內存越界 
? double free:同一塊內存釋放兩次 
? use after free:內存釋放后使用 
? wild free:釋放內存的參數為非法值 
? access uninitialized memory:訪問未初始化內存 
? read invalid memory:讀取非法內存,本質上也屬于內存越界 
? memory leak:內存泄露 
? use after return:caller訪問一個指針,該指針指向callee的棧內內存 
? stack overflow:棧溢出


常用的解決內存錯誤的方法


  • 代碼靜態檢測

       

靜態代碼檢測是指無需運行被測代碼,通過詞法分析、語法分析、控制流、數據流分析等技術對程序代碼進行掃描,找出代碼隱藏的錯誤和缺陷,如參數不匹配,有歧義的嵌套語句,錯誤的遞歸,非法計算,可能出現的空指針引用等等。統計證明,在整個軟件開發生命周期中,30%至70%的代碼邏輯設計和編碼缺陷是可以通過靜態代碼分析來發現和修復的。在C++項目開發過程中,因為其為編譯執行語言,語言規則要求較高,開發團隊往往要花費大量的時間和精力發現并修改代碼缺陷。所以C++靜態代碼分析工具能夠幫助開發人員快速、有效的定位代碼缺陷并及時糾正這些問題,從而極大地提高軟件可靠性并節省開發成本。


靜態代碼分析工具的優勢:


1、自動執行靜態代碼分析,快速定位代碼隱藏錯誤和缺陷。

2、幫助代碼設計人員更專注于分析和解決代碼設計缺陷。

3、減少在代碼人工檢查上花費的時間,提高軟件可靠性并節省開發成本。


 一些主流的靜態代碼檢測工具,免費的cppcheck,clang static analyzer;

商用的coverity,pclint等


 各個工具性能對比: 

  http://www.51testing.com/html/19/n-3709719.html


  • 代碼動態檢測


所謂的代碼動態檢測,就是需要再程序運行情況下,通過插入特殊指令,進行動態檢測和收集運行數據信息,然后分析給出報告。


1.為了檢測內存非法使用,需要hook內存分配和操作函數。hook的方法可以是用C-preprocessor,也可以是在鏈接庫中直接定義(因為Glibc中的malloc/free等函數都是weak symbol),或是用LD_PRELOAD。另外,通過hook strcpy(),memmove()等函數可以檢測它們是否引起buffer overflow。

 
2. 為了檢查內存的非法訪問,需要對程序的內存進行bookkeeping,然后截獲每次訪存操作并檢測是否合法。bookkeeping的方法大同小異,主要思想是用shadow memory來驗證某塊內存的合法性。至于instrumentation的方法各種各樣。有run-time的,比如通過把程序運行在虛擬機中或是通過binary translator來運行;或是compile-time的,在編譯時就在訪存指令時就加入檢查操作。另外也可以通過在分配內存前后加設為不可訪問的guard page,這樣可以利用硬件(MMU)來觸發SIGSEGV,從而提高速度。

 
3.為了檢測棧的問題,一般在stack上設置canary,即在函數調用時在棧上寫magic number或是隨機值,然后在函數返回時檢查是否被改寫。另外可以通過mprotect()在stack的頂端設置guard page,這樣棧溢出會導致SIGSEGV而不至于破壞數據。


工具總結對比,常用valgrind(檢測內存泄露),gperftools(統計內存消耗)等:

  

 AddressSanitizeValgrind/MemcheckDr. MemoryMudflapGuard Pagegperftools
technologyCTIDBIDBICTILibraryLibrary
ARCHx86, ARM, PPCx86, ARM, PPC, MIPS, S390X, TILEGXx86all(?)all(?)all(?)
OSLinux, OS X, Windows, FreeBSD, Android, iOS SimulatorLinux, OS X, Solaris, AndroidWindows, LinuxLinux, Mac(?)All (1)Linux, Windows
Slowdown2x20x10x2x-40x?
Detects:






Heap OOByesyesyesyessomesome
Stack OOByesnonosomenono  
Global OOByesnono?nono
UAFyesyesyesyesyesyes
UARyes (see AddressSanitizerUseAfterReturn)nonononono
UMRno (see MemorySanitizer)yesyes?nono
Leaksyes (see LeakSanitizer)yesyes?noyes

BI: dynamic binary instrumentation
CTI: compile-time instrumentation
UMR: uninitialized memory reads
UAF: use-after-free (aka dangling pointer)
UAR: use-after-return
OOB: out-of-bounds
x86: includes 32- and 64-bit.
mudflap was removed in GCC 4.9, as it has been superseded by AddressSanitizer.
Guard Page: a family of memory error detectors (Electric fence or DUMA on Linux, Page Heap on Windows, libgmalloc on OS X)
gperftools:
various performance tools/error detectors bundled with TCMalloc. Heap checker (leak detector) is only available on Linux. Debug allocator provides both guard pages and canaryonly detectors. values for more precise detection of OOB writes, so it's better than guard page.


https://github.com/google/sanitizers/wiki/AddressSanitizerComparisonOfMemoryTools


2. C++內存管理效率問題


  1. 內存管理可以分為三個層次

    自底向上分別是:


  • 第一層:操作系統內核的內存管理-虛擬內存管理

  • 第二層:glibc層維護的內存管理算法

  • 第三層:應用程序從glibc動態分配內存后,根據應用程序本身的程序特性進行優化, 比如SGI STL allocator,使用引用計數std::shared_ptr,RAII,實現應用的內存池等等。

    

當然應用程序也可以直接使用系統調用從內核分配內存,自己根據程序特性來維護內存,但是會大大增加開發成本。


      2.  C++內存管理問題


  • 頻繁的new/delete勢必會造成內存碎片化,使內存再分配和回收的效率下降;

  • new/delete分配內存在linux下默認是通過調用glibc的api-malloc/free來實現的,而這些api是通過調用到linux的系統調用:



brk()/sbrk() // 通過移動Heap堆頂指針brk,達到增加內存目的
mmap()/munmap() // 通過文件影射的方式,把文件映射到mmap區
  • 分配內存 < DEFAULT_MMAP_THRESHOLD,走brk,從內存池獲取,失敗的話走brk系統調用

  • 分配內存 > DEFAULT_MMAP_THRESHOLD,走mmap,直接調用mmap系統調用

其中,DEFAULT_MMAP_THRESHOLD默認為128k,可通過mallopt進行設置。


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.少量系統申請次數,非常少(幾沒有) 堆碎片。
       2.由于沒有系統調用等,比通常的內存申請/釋放(比如通過malloc, new等)的方式快。
       3.可以檢查應用的任何一塊內存是否在內存池里。
       4.寫一個”堆轉儲(Heap-Dump)”到你的硬盤(對事后的調試非常有用)。
       5.可以更方便實現某種內存泄漏檢測(memory-leak detection)。

       6.減少額外系統內存管理開銷,可以節約內存;


內存管理方案實現的指標:


  • 額外的空間損耗盡量少

  • 分配速度盡可能快

  • 盡量避免內存碎片

  • 多線程性能好

  • 緩存本地化友好

  • 通用性,兼容性,可移植性,易調試等



各個內存分配器的實現都是在以上的各種指標中進行權衡選擇.


4.  一些業界主流的內存管理方案


SGI STL allocator


是比較優秀的 C++庫內存分配器(細節參考上面描述)


ptmalloc 


 是glibc的內存分配管理模塊, 主要核心技術點:

  1. Arena-main /thread;支持多線程

  2. Heap segments;for thread arena via by mmap call ;提高管理

  3. chunk/Top chunk/Last Remainder chunk;提高內存分配的局部性

  4. bins/fast bin/unsorted bin/small bin/large bin;提高分配效率



ptmalloc的缺陷


  • 后分配的內存先釋放,因為 ptmalloc 收縮內存是從 top chunk 開始,如果與 top chunk 相鄰的 chunk 不能釋放, top chunk 以下的 chunk 都無法釋放。

  • 多線程鎖開銷大, 需要避免多線程頻繁分配釋放。

  • 內存從thread的areana中分配, 內存不能從一個arena移動到另一個arena, 就是說如果多線程使用內存不均衡,容易導致內存的浪費。比如說線程1使用了300M內存,完成任務后glibc沒有釋放給操作系統,線程2開始創建了一個新的arena, 但是線程1的300M卻不能用了。

  • 每個chunk至少8字節的開銷很大

  • 不定期分配長生命周期的內存容易造成內存碎片,不利于回收。64位系統最好分配32M以上內存,這是使用mmap的閾值。


tcmalloc 


google的gperftools內存分配管理模塊, 主要核心技術點:

          

  1. thread-localcache/periodic garbagecollections/CentralFreeList;提高多線程性能,提高cache利用率

     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)并取整(大于結果的最小整數),同時是由中央頁面堆來處理   的。中央頁面堆又是一個自由列表的陣列。對于i < 256而言,第k個條目是一個由k個頁面組成的自由列表。第256個條目則是一個包含了長度>= 256個頁面的自由列表:

     

   4.  Spans:

TCMalloc管理的堆由一系列頁面組成。連續的頁面由一個“跨度”(Span)對象來表示。一個跨度可以是已被分配或者是自由的。如果是自由的,跨度則會是一個頁面堆鏈表中的一個條目。如果已被分配,它會是一個已經被傳遞給應用程序的大對象,或者是一個已經被分割成一系列小對象的一個頁面。如果是被分割成小對象的,對象的尺寸類別會被記錄在跨度中。

由頁面號索引的中央數組可以用于找到某個頁面所屬的跨度。例如,下面的跨度a占據了2個頁面,跨度b占據了1個頁面,跨度c占據了5個頁面最后跨度d占據了3個頁面。

tcmalloc的改進

  • ThreadCache會階段性的回收內存到CentralCache里。解決了ptmalloc2中arena之間不能遷移的問題。

  • Tcmalloc占用更少的額外空間。例如,分配N個8字節對象可能要使用大約8N * 1.01字節的空間。即,多用百分之一的空間。Ptmalloc2使用最少8字節描述一個chunk。

  • 更快。小對象幾乎無鎖, >32KB的對象從CentralCache中分配使用自旋鎖。并且>32KB對象都是頁面對齊分配,多線程的時候應盡量避免頻繁分配,否則也會造成自旋鎖的競爭和頁面對齊造成的浪費。

 

jemalloc 


FreeBSD的提供的內存分配管理模塊, 主要核心技術點:


   1. 與tcmalloc類似,每個線程同樣在<32KB的時候無鎖使用線程本地cache;

   2. Jemalloc在64bits系統上使用下面的size-class分類:
Small: [8], [16, 32, 48, …, 128], [192, 256, 320, …, 512], [768, 1024, 1280, …, 3840]
Large: [4 KiB, 8 KiB, 12 KiB, …, 4072 KiB]
Huge: [4 MiB, 8 MiB, 12 MiB, …]

   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的優化


  • Jmalloc小對象也根據size-class,但是它使用了低地址優先的策略,來降低內存碎片化。

  • Jemalloc大概需要2%的額外開銷。(tcmalloc 1%, ptmalloc最少8B).

  • Jemalloc和tcmalloc類似的線程本地緩存,避免鎖的競爭 .

  • 相對未使用的頁面,優先使用dirty page,提升緩存命中。





性能比較


測試環境: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.facebook.com/notes/facebook-engineering/scalable-memory-allocation-using-jemalloc/480222803919


總結


可以看出tcmalloc和jemalloc性能接近,比ptmalloc性能要好,在多線程環境使用tcmalloc和jemalloc效果非常明顯。一般支持多核多線程擴展情況下可以使用jemalloc;反之使用tcmalloc可能是更好的選擇。

可以參考:

https://sploitfun.wordpress.com/2015/02/10/understanding-glibc-malloc/comment-page-1/

http://goog-perftools.sourceforge.net/doc/tcmalloc.html

https://www.facebook.com/notes/facebook-engineering/scalable-memory-allocation-using-jemalloc/480222803919

https://blog.csdn.net/junlon2006/article/details/77854898


思考問題:

1  jemalloc和tcmalloc最佳實踐是什么?

2  內存池的設計有哪些套路?為什么?

 

 


七  C++程序內存性能測試


  1. 用系統工具抓取性能數據


  • pmap

    

    通過讀取/proc/$PID/maps 和 smaps 的數據,解析數據,生成進程的虛列內存映像和一些內存統計:


 pmap -X -p 3193131931:   ./bug_tcAddress Perm   Offset Device    Inode  Size   Rss   Pss Referenced Anonymous Swap Locked Mapping7f37e4c36000 rw-p 00000000  00:00        0    132     88     88         80        88     44      0 [heap]7fffff85c000 rw-p 00000000  00:00        0  7824  7820  7820       7820      7820    0      0 [stack]
===== ===== ===== ========== ========= ==== ====== 71396 16540 13902 16540 13048 0 0 KB


里面可以查看程序堆和棧內存大小區間,程序所占內存大小,主要是關注PSS

以下內存統計名稱解釋:

VSS:Virtual Set Size,虛擬內存耗用內存,包括共享庫的內存;

RSS:Resident Set Size,實際使用物理內存,包括共享庫;

PSS:Proportional Set Size,實際使用的物理內存,共享庫按比例分配;

USS:Unique Set Size,進程獨占的物理內存,不計算共享庫,也可以理解為將進程殺 死能釋放出的內存;


一般VSS >= RSS >= PSS >= USS, 一般統計程序的內存占用,PSS是最好的選擇,比較合理。


  • top

   

實時顯示內存當前使用情況和各個進程使用內存信息


  • free 

   

查看系統可用內存和占用情況


  • /proc/meminfo

   

 查看機器使用內存使用統計和內存硬件基本信息。


  • vmstat

    

    監控內存變化

詳細請參考man手冊:

http://linuxtools-rst.readthedocs.io/zh_CN/latest/tool/


思考問題:

1  各個工具優缺點和使用場景?

2   linux內存統計里面,劃分了哪些統計?

 

參加答案


2. valgrind  massif


堆棧分析器,指示程序中使用了多少堆內存等信息,可以幫助你減少程序內存使用量,因為更小程序更能多占cache,減少分頁,加速程序;對于需要大量內存的程序,可以讓程序能夠減少交換分區使用,加速程序。


valgrind massif 采集完數據生成數據文件,數據文件會顯示每一幀的程序使用的堆內存大小,

The Snapshot Details 顯示更多細節:

更多細節參考:

http://valgrind.org/docs/manual/ms-manual.html


3. gperftools--heap profile


gperftools工具里面的內存監控器,統計監控程序使用內存的多少,可以查看內存使用熱點,默認是100ms一次采樣。

text模式:% pprof --text test_tc  test.prof

Total: 38 samples
       7  18.4%  18.4%        7  18.4% operator delete[] (inline)
       3   7.9%  26.3%        3   7.9% PackedCache::TryGet (inline)
       3   7.9%  34.2%       37  97.4% main::{lambda#1}::operator
       3   7.9%  42.1%        5  13.2% operator new (inline)
       3   7.9%  50.0%        4  10.5% tcmalloc::CentralFreeList::ReleaseToSpans
       2   5.3%  55.3%        2   5.3% SpinLock::SpinLoop
       2   5.3%  60.5%        2   5.3% _init
       2   5.3%  65.8%        2   5.3% tcmalloc::CentralFreeList::FetchFromOneSpans
       2   5.3%  71.1%        2   5.3% tcmalloc::ThreadCache::GetThreadHeap (inline)
       2   5.3%  76.3%        2   5.3% tcmalloc::ThreadCache::ReleaseToCentralCache (inline)
       1   2.6%  78.9%        1   2.6% ProfileData::FlushTable
       1   2.6%  81.6%        4  10.5% SpinLock::Lock (inline)
       1   2.6%  84.2%        1   2.6% TCMalloc_PageMap2::get (inline)
       1   2.6%  86.8%        5  13.2% tcmalloc::CentralFreeList::ReleaseListToSpans
       1   2.6%  89.5%        6  15.8% tcmalloc::CentralFreeList::RemoveRange
       1   2.6%  92.1%        1   2.6% tcmalloc::SizeMap::GetSizeClass (inline)


  • 第一列代表這個函數調用本身直接使用了多少內存,

  • 第二列表示第一列的百分比,

  • 第三列是從第一行到當前行的所有第二列之和,

  • 第四列表示這個函數調用自己直接使用加上所有子調用使用的內存總和,

  • 第五列是第四列的百分比。


基本上只要知道這些,就能很好的掌握每一時刻程序運行內存使用情況了,并且對比不同時段的不同profile數據,可以分析出內存走向,進而定位熱點和泄漏。


pdf模式:可以把采樣的結果轉換為圖模式,這樣查看更為直觀:


   

Kcachegrind模式:利用pprof生成callgrind格式的文件即可,KCachegrind的GUI工具,用于分析callgrind


  • 圖形化地瀏覽源碼和執行次數,并使用各種排序來搜索可優化的東西。

  • 分析不同的圖表,來可視化地觀察什么占據了大多數時間,以及它調用了什么。

  • 查看真實的匯編機器碼輸出,使你能夠看到實際的指令,給你更多的線索。

  • 可視化地顯示源碼中的循環和分支的跳躍方式,便于你更容易地找到優化代碼的方法。


更多細節參考  

https://github.com/gperftools/gperftools/blob/master/docs/heapprofile.html

windows 版本:

https://sourceforge.net/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://herbsutter.com/gotw/_102/

https://lanzkron.wordpress.com/2012/04/22/make_shared-almost-a-silver-bullet/

http://en.cppreference.com/w/cpp/memory/shared_ptr/make_shared


關注程序喵大人,C++學習不迷路!





往期推薦


1、少寫點
if-else吧,它的效率有多低你知道嗎?
2、年度原創好文匯總
3、深度好文|面試官:進程和線程,我只問這19個問題
4
他來了,他來了,C+
+17新特性精華都在這了
5一文讓你搞懂設計模式
6、C++11新特性,所有知識點都在這了!





C++學習資料免費獲取方法:關注程序喵大人,后臺回復“程序喵”即可免費獲取40萬字C++進階獨家學習資料。



亚洲欧美第一页_禁久久精品乱码_粉嫩av一区二区三区免费野_久草精品视频
久久久久久久激情视频| 香港久久久电影| 玖玖精品视频| 欧美黄色影院| 18成人免费观看视频| 久久综合国产精品台湾中文娱乐网| 欧美一区二区三区视频免费播放| 久久精品国产一区二区三| 亚洲午夜电影在线观看| 亚洲精华国产欧美| 国产亚洲女人久久久久毛片| 欧美一区二区三区啪啪| 国产色婷婷国产综合在线理论片a| 亚洲国产小视频| 国产精品婷婷午夜在线观看| 国产精品国产自产拍高清av王其| 国产精品久久77777| 欧美国产91| 国产亚洲激情| 一区二区三区|亚洲午夜| 日韩视频第一页| a4yy欧美一区二区三区| 亚洲人久久久| 亚洲欧美日韩国产成人| 久久亚洲欧洲| 欧美高清在线播放| 羞羞色国产精品| 红桃视频成人| 国产综合久久| 欧美三日本三级三级在线播放| 六月婷婷久久| 欧美v亚洲v综合ⅴ国产v| 亚洲高清不卡一区| 欧美午夜精品久久久久久人妖| 在线观看亚洲视频| 亚洲激情成人在线| 在线播放亚洲一区| 欧美99在线视频观看| 久久久福利视频| 久久亚洲欧美| 欧美日韩另类在线| 免费影视亚洲| 亚洲视频一二| 亚洲第一色中文字幕| 国产免费亚洲高清| 国产精品一区亚洲| 欧美在线精品一区| 亚洲男人影院| 国产精品草草| 亚洲欧美春色| 欧美日韩精品一区二区天天拍小说| 亚洲一区二区三区影院| 亚洲人www| 久久aⅴ国产欧美74aaa| 亚洲男女自偷自拍| 欧美激情影音先锋| 国产精品系列在线| 亚洲视频第一页| 亚洲一区二区三区乱码aⅴ蜜桃女| 国产精品你懂的在线| 亚洲精品网址在线观看| 在线免费观看欧美| 久久精品国产欧美亚洲人人爽| 欧美成人情趣视频| 欧美激情2020午夜免费观看| 国产日韩欧美二区| 国产一区二区三区最好精华液| 久久只精品国产| 欧美激情2020午夜免费观看| 欧美精品九九99久久| 狠狠色噜噜狠狠色综合久| 狠狠色狠狠色综合日日五| 欧美日韩三级在线| 美女性感视频久久久| 亚洲免费电影在线| 亚洲欧美清纯在线制服| 在线电影国产精品| 欧美成va人片在线观看| 欧美三级视频在线播放| 国产精品区二区三区日本| 国产美女精品免费电影| 亚洲在线观看视频网站| 亚洲欧美日本另类| 亚洲国产毛片完整版| 欧美视频在线观看视频极品| 亚洲欧美日韩精品在线| 欧美黑人在线观看| 亚洲国产精品v| 欧美小视频在线观看| 欧美日韩国产区| 亚洲另类自拍| 国产精品美女久久久久aⅴ国产馆| 欧美精品一区二区精品网 | 久久精品一区蜜桃臀影院| 国产一区白浆| 久久综合网hezyo| 中文成人激情娱乐网| 国产精品九色蝌蚪自拍| 国产精品久久影院| 欧美日韩一区二区在线| 亚洲一区二区免费| 亚洲精品三级| 国产精品福利久久久| 1769国产精品| 一区二区激情视频| 欧美三级在线| 亚洲天堂av在线免费观看| 国产精品户外野外| 亚洲欧美日韩一区二区| 国产日本欧美在线观看| 久久精品一区二区三区不卡牛牛 | 欧美二区乱c少妇| 91久久极品少妇xxxxⅹ软件| 欧美日本视频在线| 亚洲一区中文| 国产视频一区在线观看| 麻豆成人在线| 在线中文字幕日韩| 国内精品伊人久久久久av影院 | 亚洲第一页自拍| 欧美激情一区二区三区蜜桃视频| 一区二区欧美国产| 国产视频亚洲精品| 狼人社综合社区| 亚洲一级在线观看| 国内成+人亚洲| 欧美日韩视频在线一区二区 | 欧美一区国产一区| 国产一区二区按摩在线观看| 午夜视黄欧洲亚洲| 国产日韩欧美综合| 久久免费少妇高潮久久精品99| 亚洲缚视频在线观看| 韩国一区二区三区美女美女秀| 久久亚洲春色中文字幕| 亚洲国产成人高清精品| 国产精品国产三级国产aⅴ无密码 国产精品国产三级国产aⅴ入口 | 国产区欧美区日韩区| 最近看过的日韩成人| 国产视频在线观看一区二区三区| 亚洲国产精品悠悠久久琪琪| 国内成人精品视频| 国产亚洲精品一区二555| 国产精品视频成人| 欧美日韩在线一区二区| 欧美丝袜第一区| 国产精品色婷婷久久58| 国产精品视频一二三| 国产午夜精品久久久| 国产精品va在线| 国产精品国产| 国产精品乱码人人做人人爱| 影音欧美亚洲| 在线观看福利一区| 亚洲精品日本| 亚洲一级二级| 久久色在线播放| 欧美日韩在线亚洲一区蜜芽| 欧美精品乱码久久久久久按摩| 欧美激情久久久| 国产精品va在线播放| 亚洲人成欧美中文字幕| 亚洲午夜在线观看| 久久美女艺术照精彩视频福利播放| 久久精品人人做人人爽| 国产欧美一区二区三区久久 | 亚洲日本在线视频观看| 日韩一区二区精品在线观看| 久久av一区二区三区漫画| 欧美日韩国产精品成人| 一区二区三区中文在线观看 | 久久久久国色av免费观看性色| 久久综合给合| 国产精品欧美久久久久无广告| 激情成人av| 欧美黄色免费网站| 亚洲电影专区| 一区二区三区成人精品| 蜜臀久久99精品久久久久久9| 国产精品久久久久一区二区三区共 | 国产主播一区二区三区| 亚洲女人av| 国产精品久久久久久久久| 中文一区二区| 国产精品女人网站| 欧美在线观看日本一区| 国产伪娘ts一区| 久久久国产精品亚洲一区| 老鸭窝亚洲一区二区三区| 亚洲日本免费电影| 欧美理论在线播放| 亚洲视频免费看| 一区二区三区自拍| 欧美另类69精品久久久久9999| 亚洲第一搞黄网站| 欧美激情精品久久久久久蜜臀| 亚洲欧美日韩一区二区在线 | 黄色另类av| 欧美成人综合网站| 亚洲美女精品成人在线视频|