C++內存模型


                                                                         C++內存模型php

                                                             一文了解全部C++內存的問題html

                                                                            AlexCooljava

 

目錄node

一  C++內存模型linux

二  C++對象內存模型ios

三 C++程序運行內存空間模型c++

四  C++棧內存空間模型git

五 C++堆內存空間模型程序員

六  C++內存問題及經常使用的解決方法github

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

 

環境:

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++ 抽象機的目的定義計算機內存存儲的語義。

可用於 C++ 程序的內存是一或多個相接的字節序列。內存中的每一個字節擁有惟一的地址

字節

字節是最小的可尋址內存單元。它被定義爲相接的位序列,大到足以保有任何 UTF-8 編碼單元( 256 個相異值)和 (C++14 起)基本執行字符集(要求爲單字節的 96 個字符)的任何成員。相似 C , C++ 支持 8 位或更大的字節。

char 、 unsigned char 和 signed char 類型把一個字節用於存儲和值表示。字節中的位數可做爲 CHAR_BITstd::numeric_limits<unsigned char>::digits 訪問。

內存位置

內存位置

  • 一個標量類型(算術類型、指針類型、枚舉類型或 std::nullptr_t )對象
  • 或非零長位域的最大相接序列

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

線程與數據競爭

執行線程是程序中的控制流,它始於 std::thread::threadstd::async 或以其餘方式所作的頂層函數調用。

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

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

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

  • 兩個求值都在同一線程上,或同一信號處理函數中執行,或
  • 兩個衝突求值都是原子操做(見 std::atomic ),或
  • 一個衝突求值先發生於( happens-before )另外一個(見 std::memory_order

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

(特別是, std::mutex 的釋放同步於,從而先發生於另外一線程取得同一 mutex ,這使得能夠用互斥鎖防止數據競爭)

內存順序

線程在從內存位置讀取值時,它可能看到初值、同一線程所寫入的值或另外一線程寫入的值。線程所做的寫入對其餘線程變爲可見的順序上的細節,見 std::memory_order

from  https://zh.cppreference.com/w/cpp/language/memory_model

思考問題:

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

2  內存對程序併發執行有什麼影響?

std::memory_order 的做用是什麼?

 

參考答案:

二  C++對象內存模型

1 空類對象(通常做爲模板的tag來使用,參考《模板的本質》)

class A { };
sizeof(A) = 1
C++標準要求C++的對象大小不能爲0,C++對象必須在內存裏面有惟一的地址,
但又不想浪費太多內存空間,因此標準規定爲1byte,
A --> +-----------+
| 1 bytes |
+-----------+
 

3  非空類

class A 
{
public:
   int a;
};
sizeof(A ) = 8  ,align=8
A --> +-----------+
|pad | a |
+-----------+

3  非空虛基類

class A 
{
public:
   int a;
   virtual void v();
};

sizeof(A ) = 16  ,align=8

                                                             vtable

                           +-----------------------+
| 0 (top_offset) |
+-----------------------+
A --> +----------+ | ptr to typeinfo for A |
| vtptr |-------> +-----------------------+
+----------+ | A::v() |
| pad |a | +-----------------------+
+----------+
 

4 單繼承

class A {
public:
int a;
virtual void v();
};

class B : public A {
public:
int b;
};
sizeof(B) = 16, align = 8
                                    vtable
 
                           +-----------------------+
| 0 (top_offset) |
+-----------------------+
b --> +----------+ | ptr to typeinfo for B |
| vtptr |-------> +-----------------------+
+----------+ | A::v() |
| b | a | +-----------------------+
+----------+

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
                                    vtable
                           +-----------------------+
| 0 (top_offset) |
+-----------------------+
c --> +----------+ | ptr to typeinfo for C |
| vtptr |-------> +-----------------------+
+----------+ | A::v() |
| pad |a | +-----------------------+
+----------+ | -16 (top_offset) |
| vtptr |---+ +-----------------------+
+----------+ | | ptr to typeinfo for C |
| c | b | +---> +-----------------------+
+----------+ | B::w() |
+-----------------------+

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
                                    vtable
                           +-----------------------+
| 0 (top_offset) |
+-----------------------+
c --> +----------+ | ptr to typeinfo for C |
| vtptr |-------> +-----------------------+
+----------+ | A::v() |
      | pad |a   |         +-----------------------+
      +----------+         |         C::w()        |
| vtptr |---+ +-----------------------+
+----------+  | | -16 (top_offset) |
| c | b |  | +-----------------------+
+----------+ | | ptr to typeinfo for C |
+---> +-----------------------+
   | thunk to C::w()  |
+-----------------------+

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
                                   vtable
                           +-----------------------+
| 0 (top_offset) |
+-----------------------+
d --> +----------+ | ptr to typeinfo for D |
| vtptr |-------> +-----------------------+
+----------+ | A::v() |
| b |a | +-----------------------+
+----------+ | B::w() |
| vtptr |---+ +-----------------------+
+----------+ | | D::y() |
| c |a | | +-----------------------+
+----------+ | | -16 (top_offset) |
| d | | +-----------------------+
+----------+ | | ptr to typeinfo for D |
+---> +-----------------------+
| A::v() |
+-----------------------+
| C::x() |
+-----------------------+
注意點:
1  此種繼承,存在兩份份基類成員,使用時候須要指定路徑,使用不方便,易出錯。
 
 

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                  vtable  
                                   +-----------------------+
| 32 (vbase_offset) |
+-----------------------+
| 0 (top_offset) |
+-----------------------+
| ptr to typeinfo for D |
+----------> +-----------------------+
d --> +----------+ | | B::w() |
| vtptr |----+ +-----------------------+
+----------+ | D::y() |
|pad |b | +-----------------------+
+----------+ | 16 (vbase_offset) |
| vtptr |---------+ +-----------------------+
+----------+ | | -16 (top_offset) |
| d | c | | +-----------------------+
+----------+ | | ptr to typeinfo for D |
| vtptr |----+ +-----> +-----------------------+
+----------+ | | C::x() |
| pad | a | | +-----------------------+
+----------+ | | 0 (vbase_offset) |
| +-----------------------+
| | -32 (top_offset) |
| +-----------------------+
| | ptr to typeinfo for D |
+----------> +-----------------------+
| A::v() |
+-----------------------+
注意點:
1 top_offset 表示this指針對子類的偏移,用於子類和繼承類之間dynamic_cast轉換(還須要typeinfo數據),實現多態,
  vbase_offset 表示this指針對基類的偏移,用於共享基類;
2 gcc爲了每個類生成一個vtable虛函數表,放在程序的.rodata段,其餘編譯器(平臺)好比vs,實現不太同樣.
3 gcc還有VTT表,裏面存放了各個基類之間虛函數表的關係,最大化利用基類的虛函數表,專門用來爲構建最終類vtable;
4 在構造函數裏面設置對象的vtptr指針。
4 虛函數表地址的前面設置了一個指向type_info的指針,RTTI(Run Time Type Identification)運行時類型識別是有編譯器在編譯器生成的特殊類型信息,包括對象繼承關係,對象自己的描述,RTTI是爲多態而生成的信息,因此只有具備虛函數的對象在會生成。
5 在C++類中有兩種成員數據:static、nonstatic;三種成員函數:static、nonstatic、virtual。
  C++成員非靜態數據須要佔用動態內存,棧或者堆中,其餘static數據存在全局變量區(數據段),編譯時候肯定。
  虛函數會增長用虛函數表大小,也是存儲在數據區的.rodada段,編譯時肯定,其餘函數不佔空間。
6 G++ 選項 -fdump-class-hierarchy 能夠生成C++類層結構,虛函數表結構,VTT表結構。
7 GDB調試選項:
    set p obj <on/off> 在C++中,若是一個對象指針指向其派生類,若是打開這個選項,GDB會如今類對象結構的規則顯示輸出。
    set p pertty <on/off>: 按照層次打印結構體。
思考問題:
1 Why don't we have virtual constructors?

2  爲何不要在構造函數或者析構函數中調用虛函數?

3  C++對象構造順序?

4  爲何虛函數會下降效率?

 

參考答案:

1 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 構造大體順序:

1.構造子類構造函數的參數

2.子類調用基類構造函數

3.基類設置vptr

4.基類初始化列表內容進行構造

5.  基類函數體調用

6.  子類設置vptr

7.  子類初始化列表內容進行構造

8.  子類構造函數體調用

4  是由於虛函數調用,執行過程當中會跳轉兩次,首先找到虛函數表,而後再查找對應函數地址,這樣CPU指令就會跳轉兩次,而普通函數指跳轉一次,CPU每跳轉一次,預取指令均可能做廢,這會致使分支預測失敗,流水線排空,因此效率會變低。

設想一下,若是說不是虛函數,那麼在編譯時期,其相對地址是肯定的,編譯器能夠直接生成jmp/invoke指令; 若是是虛函數,多出來的一次查找vtable所帶來的開銷,卻是次要的,關鍵在於,這個函數地址是動態的,譬如 取到的地址在eax裏,則在call eax以後的那些已經被預取進入流水線的全部指令都將失效。流水線越長,一次分支預測失敗的代價也就越大。

 

三  C++程序運行內存空間模型

1. C++程序大體運行空間:

32位:

 

Flexible Process Address Space Layout In Linux

   from  https://manybutfinite.com/post/anatomy-of-a-program-in-memory/

64位:

x86_process_address_space.png

from  http://www.cnhalo.net/2016/06/13/memory-optimize/

2 Linux虛擬內存內部實現

   from http://www.javashuo.com/article/p-slspvzbc-bz.html

關鍵點:

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
from  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表示共享段,若是沒有相應權限,則用’-’代替

 

  from  https://blog.csdn.net/lijzheng/article/details/23618365

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

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

1 #include<iostream>
  2 #include <unistd.h>
  3 using namespace std;
  4 //long  a[1024*1024] = {0};
  5 int main()
  6 {
  7     void *heap;
  8     int *x = new int[1024]();
  9     cout << hex <<"x: " << x <<endl;
10     heap = sbrk(0);
11     //cout << hex << "a:" << (long) &a <<endl;
12     cout << hex << "heap: " << (long) heap <<endl;
13     cout << hex << "heap: " << (long)heap - (long)x <<endl;
14     while(1);
15     return 0;
16 }

g++  -g  -std=c++11 -o main  mem.cpp

./main

關閉了內存地址隨機化

pmap -X 8117
8117:   ./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的耦合。

from  https://lwn.net/Articles/615809/
     7ffff7ffc000 r--p 00023000  08:11   266344     4    4   4          4         4    0      0 ld-2.21.so
     7ffff7ffd000 rw-p 00024000  08:11   266344     4    4   4          4         4    0      0 ld-2.21.so

     7ffff7ffe000 rw-p 00000000  00:00        0     4    4   4          4         4    0      0
     7ffffffde000 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  http://www.javashuo.com/article/p-gsrstegm-hs.html

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

      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),並跳轉到此位置執行;

3  棧攻擊

   由上面棧內存佈局能夠看出,棧很容易被破壞和攻擊,經過棧緩衝器溢出攻擊,用攻擊代碼首地址來替換函數幀的返回地址,當子函數返回時,便跳轉到攻擊代碼處執行,獲取系統的控制權,因此操做系統和編譯器採用了一些經常使用的防攻擊的方法:

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

  • Cannary 

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

     ·

 

            from   http://walkerdu.com/2017/04/21/gcc-stack-overflow-check/

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

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

4 棧異常處理

  • 一個函數(或方法)拋出異常,那麼它首先將當前棧上的變量所有清空(unwinding),若是變量是類對象的話,將調用其析構函數,接着,異常來到call stack的上一層,作相同操做,直到遇到catch語句。
  • 指針是一個普通的變量,不是類對象,因此在清空call stack時,指針指向資源的析構函數將不會調用。

思考問題:

1  遞歸調用函數怎麼從20層直接返回到17層,程序能夠正常運行?

2  調用約定有哪些?

 

參考答案:

1

  • 參考上面棧幀的結構,中心思想是當遞歸函數執行到第20層的時候,把當前棧幀的rbp值替換爲17層的rbp的值,

       怎麼獲得17層rbp的值, 就是經過反覆取rbp的值(rbp保持了上一幀的rbp);核心代碼以下:

  4 /*change stack*/
  5 int ret_stack(int layer)
  6 {
  7     unsigned long rbp = 0;
  8     unsigned long layer_rbp = 0;
  9     int depth = 0;
 10
 11     /* 1.獲得首層函數的棧基址 */
 12     __asm__ volatile(
 13             "movq %%rbp, %0 \n\t"
 14             :"=r"(rbp)
 15             :
 16             :"memory");
 17
 18     layer_rbp = rbp;
 19     cout << hex<< rbp <<endl;
 20     /* 2.逐層回溯棧基址 */
 21     for(; (depth < layer) && (0 != layer_rbp) && (0 != *(unsigned long *)layer_rbp) && (layer_rbp != *(unsigned long *)layer_rbp); ++depth) {
 22         cout << hex<< layer_rbp <<endl;
 23         layer_rbp = *(unsigned long *)layer_rbp;
 24     }
 25     cout << hex<< layer_rbp <<endl;
 26     //change current rbp to target layer rbp
 27     unsigned long *x = (unsigned long *)rbp;
 28     *x = layer_rbp;
 29     cout << hex<< x << " v:" << *x <<endl;
 30     return depth;
 31 }

2

在這些調用約定中,咱們最經常使用是如下幾種約定
1. cdecl
2. stdcall
3. thiscall

cdecl 是c/c++默認的調用約定。
stdcall 它是微軟Win32 API的一準標準,咱們經常使用的回調函數就是經過這種調用方式
thiscall 是c++中非靜態類成員函數的默認調用約定

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

五  C++堆內存空間模型

1. C++ 程序動態申請內存new/delete:

  1. new/delete 操做符,C++內置操做符:

           1. new操做符作兩件事,分配內存+調用構造函數初始化。你不能改變它的行爲;

           2.  delete操做符一樣作兩件事,調用析構函數+釋放內存。你不能改變它的行爲;

          2.  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 thestd namespace.
  • 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.

Ifset_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分配失敗會拋異常(默認),經過傳遞參數也能夠不拋異常,返回空指針;

    3.  placement new/delete 函數

                1. placement new 其實就是new的一種重載,placement new是一種特殊的operator new,做用於一塊已分配但未處理或未初始化的raw內存,就是用一塊已經分配好的內存上重建對象(調用構造函數);

                2. 它是C++庫標準的一部分;

                3. placement delete 什麼都不作;

    4.  數組分配 new[]/delete[] 表達式

       1. 對應會調用operator new[]/delete[]函數;

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

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

5  class-specific allocation functions

 

class-specific allocation functions
   
void* T::operator new  ( std::size_t count );
(15)  
void* T::operator new[]( std::size_t count );
(16)  
void* T::operator new  ( std::size_t count, std::align_val_t al );
(17) (since C++17)
void* T::operator new[]( std::size_t count, std::align_val_t al );
(18) (since C++17)
class-specific placement allocation functions
   
void* T::operator new  ( std::size_t count, user-defined-args... );
(19)  
void* T::operator new[]( std::size_t count, user-defined-args... );
(20)  
void* T::operator new  ( std::size_t count,
                         std::align_val_t al, user-defined-args... );
(21) (since C++17)
void* T::operator new[]( std::size_t count,
                         std::align_val_t al, user-defined-args... );
(22) (since C++17)

from 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++能夠爲分配失敗設置本身的異常處理函數:

  Ifset_new_handlerhas been used to define anew_handlerfunction, 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-expression withnothrow, 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的智能指針(smart_ptr)與垃圾回收:

  • C++智能指針出現是爲了解決因爲支持動態內存分配而致使的一些C++內存問題,好比內存泄漏,

       對象生命週期的管理,懸掛指針(dangling pointer)/空指針等問題;

  • C++智能指針經過RAII設計模式去管理對象生命週期(動態內存管理),提供帶少許異常相似普通指針的操做接口,

       在對象構造的時候分配內存,在對象做用域以外釋放內存,幫助程序員管理動態內存;

  • 老的智能指針auto_ptr因爲設計語義很差而致使不少不合理問題: 不支持複製(拷貝構造函數)和賦值(operator =),但複製或賦值的時候不會提示出錯。由於不能被複制,因此不能被放入容器中。而被C++11棄用(deprecated),新的智能指針以下:

    1. shared_ptr

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

              

             from http://www.cppblog.com/Solstice/archive/2013/01/28/197597.html

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

  •   enable_shared_from_this模板類,用來返回this指針的shared_ptr版本。

shared_ptr關鍵點:

    1. 用shared_ptr就不要new,保證內存管理的一致性;
    2. 使用weak_ptr來打破循環引用;
    3. 用make_shared來生成shared_ptr:

            1. 提升效率,內存分配一次搞定(對象數據和ref_count控制模塊);

            2. 防止異常致使內存泄漏,參考https://herbsutter.com/gotw/_102/;

            3. 因爲一次性分配內存,對象數據和ref_count控制模塊生命週期被綁定在一塊兒,須要等到全部的weak引用爲0時才能最終釋放內存(delete),當use_count爲0時只會調用析構函數;//from http://en.cppreference.com/w/cpp/memory/shared_ptr/make_shared,因此對象內存的佔用時間比較長;

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

           5. 不能在對象的構造函數中使用shared_from_this()函數,爲何

    由於對象尚未構造完畢,share_ptr尚未初始化構造徹底。 構造順序:先須要調用enable_shared_from_this類的構造函數,接着調用對象的構造函數,最後須要調用shared_ptr類的構造函數初始化enable_shared_from_this的成員變量weak_this_。而後才能使用shared_from_this()函數。

           6. 大量的shared_ptr會致使程序性能降低(相對其餘指針)。

 

    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對象能夠被函數返回。

      from https://blog.csdn.net/booirror/article/details/44455293    

   3. weak_ptr

思考問題:

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

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

2  smart_ptr有哪些坑能夠仍然致使內存泄漏?

   1. shared_ptr初始化構造函數指針,通常是能夠動態管理的內存地址,若是不是就可能致使內存泄漏;

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

   3. shared_ptr對象和其餘大多數STL容器同樣,自己不是線程安全的,須要用戶去保證;

3  unique_ptr有哪些限制?

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

    2. 不支持類型轉換(cast);

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

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

1.不泄露任何資源

2.不容許數據被破壞

智能指針就是採用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.

from 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.

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

標準垃圾回收

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.open-std.org/jtc1/sc22/wg21/docs/papers/2008/n2585.pdf

參考問題:

1  C++能夠經過哪些技術來支持「垃圾回收」?

    smart_ptr,RAII, move語義等;

2  RAII是指什麼?

  RAII是指Resource Acquisition Is Initialization的設計模式,

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

當一個函數須要經過多個局部變量來管理資源時,RAII就顯得很是好用。由於只有被構形成功(構造函數沒有拋出異常)的對象纔會在返回時調用析構函數[4],同時析構函數的調用順序剛好是它們構造順序的反序[5],這樣既能夠保證多個資源(對象)的正確釋放,又能知足多個資源之間的依賴關係。

因爲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

   from  https://zcheng.ren/2016/08/16/STLAllocater/#%E7%AC%AC%E4%BA%8C%E7%BA%A7%E9%85%8D%E7%BD%AE%E5%99%A8

4。

思考問題:

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++靜態代碼分析工具可以幫助開發人員快速、有效的定位代碼缺陷並及時糾正這些問題,從而極大地提升軟件可靠性並節省開發成本。

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

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

  二、幫助代碼設計人員更專一於分析和解決代碼設計缺陷。

  三、減小在代碼人工檢查上花費的時間,提升軟件可靠性並節省開發成本。

      一些主流的靜態代碼檢測工具:

       免費的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而不至於破壞數據。

     Tools Summary:

   

 

  AddressSanitizer Valgrind/Memcheck Dr. Memory Mudflap Guard Page gperftools
technology CTI DBI DBI CTI Library Library  
ARCH x86, ARM, PPC x86, ARM, PPC, MIPS, S390X, TILEGX x86 all(?) all(?) all(?)  
OS Linux, OS X, Windows, FreeBSD, Android, iOS Simulator Linux, OS X, Solaris, Android Windows, Linux Linux, Mac(?) All (1) Linux, Windows  
Slowdown 2x 20x 10x 2x-40x ?  
Detects:              
Heap OOB yes yes yes yes some some  
Stack OOB yes no no some no no    
Global OOB yes no no ? no no  
UAF yes yes yes yes yes yes  
UAR yes (see AddressSanitizerUseAfterReturn) no no no no no  
UMR no (see MemorySanitizer) yes yes ? no no  
Leaks yes (see LeakSanitizer) yes yes ? no yes  

DBI: 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 canary values for more precise detection of OOB writes, so it's better than guard page-only detectors.

from 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的系統調用:

圖片描述

               from https://sploitfun.wordpress.com/2015/02/11/syscalls-used-by-malloc/

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) 所示。

          

                         from http://tencentdba.com/blog/linux-virtual-memory-glibc/

       因此free的內存不必定真正的歸還給OS,隨着系統頻繁地 malloc 和 free ,尤爲對於小塊內存,堆內將產生愈來愈多不可用的碎片,致使「內存泄露」。而這種「泄露」現象使用 valgrind 是沒法檢測出來的。

        from http://tencentdba.com/blog/linux-virtual-memory-glibc/

  • 綜上,頻繁內存分配釋放還會致使大量系統調用開銷,影響效率,下降總體性能;
      3. 經常使用解決上述問題的方案
  • 內存池技術

    內存池方案一般一次從系統申請一大塊內存塊,而後基於在這塊內存塊能夠進行不一樣內存策略實現,

    能夠比較好得解決上面提到的問題,通常採用內存池有如下好處:

       1.少許系統申請次數,很是少(幾沒有) 堆碎片。
       2.因爲沒有系統調用等,比一般的內存申請/釋放(好比經過malloc, new等)的方式快。
       3.能夠檢查應用的任何一塊內存是否在內存池裏。
       4.寫一個」堆轉儲(Heap-Dump)」到你的硬盤(對過後的調試很是有用)。
       5.能夠更方便實現某種內存泄漏檢測(memory-leak detection)。

       6.減小額外系統內存管理開銷,能夠節約內存;

  • 內存管理方案實現的指標有:
  1. 額外的空間損耗盡可能少
  2. 分配速度儘量快
  3. 儘可能避免內存碎片
  4. 多線程性能好
  5. 緩存本地化友好
  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;提升分配效率

      等等

  • tcmalloc 是google的gperftools內存分配管理模塊

        主要核心技術點:

         

      from  http://gao-xiao-long.github.io/2017/11/25/tcmalloc/

  1. thread-local cache/periodic garbage collections/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個頁面。

from  http://gao-xiao-long.github.io/2017/11/25/tcmalloc/

tcmalloc的改進

  • ThreadCache會階段性的回收內存到CentralCache裏。 解決了ptmalloc2中arena之間不能遷移的問題。
  • Tcmalloc佔用更少的額外空間。例如,分配N個8字節對象可能要使用大約8N * 1.01字節的空間。即,多用百分之一的空間。Ptmalloc2使用最少8字節描述一個chunk。
  • 更快。小對象幾乎無鎖, >32KB的對象從CentralCache中分配使用自旋鎖。 而且>32KB對象都是頁面對齊分配,多線程的時候應儘可能避免頻繁分配,不然也會形成自旋鎖的競爭和頁面對齊形成的浪費。

 

  • jemallocFreeBSD的提供的內存分配管理模塊

      主要核心技術點:

   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,提高緩存命中。

一些主流的內存分配方案性能比較:

from 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

http://www.javashuo.com/article/p-fyeutdio-bk.html

思考問題:

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

2  心裏池的設計有哪些套路?爲何?

 

參考答案:

 

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

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

  •      pmap

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

 pmap -X -p 31931
31931:   ./bug_tc
Address Perm   Offset Device    Inode  Size   Rss   Pss Referenced Anonymous Swap Locked Mapping
  …

7f37e4c36000 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 採集完數據生成數據文件,數據文件會顯示每一幀的程序使用的堆內存大小:

   MB
3.952^                                                                    # 
     |                                                                   @#:
     |                                                                 :@@#:
     |                                                            @@::::@@#: 
     |                                                            @ :: :@@#::
     |                                                          @@@ :: :@@#::
     |                                                       @@:@@@ :: :@@#::
     |                                                    :::@ :@@@ :: :@@#::
     |                                                    : :@ :@@@ :: :@@#::
     |                                                  :@: :@ :@@@ :: :@@#:: 
     |                                                @@:@: :@ :@@@ :: :@@#:::
     |                           :       ::         ::@@:@: :@ :@@@ :: :@@#:::
     |                        :@@:    ::::: ::::@@@:::@@:@: :@ :@@@ :: :@@#:::
     |                     ::::@@:  ::: ::::::: @  :::@@:@: :@ :@@@ :: :@@#:::
     |                    @: ::@@:  ::: ::::::: @  :::@@:@: :@ :@@@ :: :@@#:::
     |                    @: ::@@:  ::: ::::::: @  :::@@:@: :@ :@@@ :: :@@#:::
     |                    @: ::@@:::::: ::::::: @  :::@@:@: :@ :@@@ :: :@@#:::
     |                ::@@@: ::@@:: ::: ::::::: @  :::@@:@: :@ :@@@ :: :@@#:::
     |             :::::@ @: ::@@:: ::: ::::::: @  :::@@:@: :@ :@@@ :: :@@#:::
     |           @@:::::@ @: ::@@:: ::: ::::::: @  :::@@:@: :@ :@@@ :: :@@#:::
   0 +----------------------------------------------------------------------->Mi
     0                                                                   626.4

Number of snapshots: 63
 Detailed snapshots: [3, 4, 10, 11, 15, 16, 29, 33, 34, 36, 39, 41,
                      42, 43, 44, 49, 50, 51, 53, 55, 56, 57 (peak)]
The Snapshot Details 顯示更多細節
--------------------------------------------------------------------------------
  n        time(B)         total(B)   useful-heap(B) extra-heap(B)    stacks(B)
--------------------------------------------------------------------------------
 10         10,080           10,080           10,000            80            0
 11         12,088           12,088           12,000            88            0
 12         16,096           16,096           16,000            96            0
 13         20,104           20,104           20,000           104            0
 14         20,104           20,104           20,000           104            0
99.48% (20,000B) (heap allocation functions) malloc/new/new[], --alloc-fns, etc.
->49.74% (10,000B) 0x804841A: main (example.c:20)
| 
->39.79% (8,000B) 0x80483C2: g (example.c:5)
| ->19.90% (4,000B) 0x80483E2: f (example.c:11)
| | ->19.90% (4,000B) 0x8048431: main (example.c:23)
| |   
| ->19.90% (4,000B) 0x8048436: main (example.c:25)
|   
->09.95% (2,000B) 0x80483DA: f (example.c:10)
  ->09.95% (2,000B) 0x8048431: main (example.c:23)

更多細節參考 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,嵌入式設備)性能影響?

 

 

參考答案:

 

參考:

http://www.javashuo.com/article/p-gsrstegm-hs.html

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

相關文章
相關標籤/搜索