stl 內存管理【轉】

有感於STL的內存管理
轉自 http://blog.163.com/dengminwen@126/blog/static/870226720097189486788/
web技術   2009-08-18 09:48   閱讀244   評論4 
字號: 大  中  小
警告:本文是技術類文章,只適合碼工們圍觀,非碼工請跳過此坑


1. 背景

前些天在一個技術分享會上,某大牛說,STL使用了內存池,釋放內存的時候,並不釋放給OS,而是自己由留着用。

聽到這些觀點後,我就有些着急了,因爲我以前一直是直接使用STL的一些工具類的,比如std::string、std::map、std::vector、std::list等等,從來都沒有關注過內存的問題。

帶着內存的問題,我花了兩三天的時間去閱讀STL的代碼,並且寫一些簡單的程序進行測試;下面列舉一些心得體會,但是卻沒有什麼大的結論 -.-


2. 容易誤解的簡單例子

我們以STL中的map爲例,下面有一個使用map的簡單例子,大部分人可以在30秒內寫好。


void testmap()
{
  map<int, float> testmap;
  for (int i = 0; i < 1000000; i++) {
    testmap[i] = (float)i;
  }
  testmap.clear();
}


爲了在調用map::clear()之後查看進程的內存使用量,我們可以加幾行代碼讓程序暫停一下。


void testmap()
{
  map<int, float> testmap;
  for (int i = 0; i < 1000000; i++) {
    testmap[i] = (float)i;
  }
  testmap.clear();
  // 觀察點
  int tmp; cout << "use ps to see my momory now, and enter int to continue:"; cin >> tmp;
}


編譯運行上面的程序,你會看見這樣的情況:ps顯示進程的內存使用量爲40MB多。這時,你會毫不猶豫地說,STL的map使用了內存池(memory pool)。

然後,我就跑去閱讀libstdc++的STL的源代碼,STL提供了很多種Allocator的實現,有基於內存池的,但是默認的std::allocator的實現是new_allocator,這個實現只是簡單的對new和delete進行了簡單的封裝,並沒有使用內存池。這樣,懷疑的對象就轉移到glibc的malloc函數了。malloc提供的兩個函數來查看當前申請的內存的狀態,分別是malloc_stats()和mallinfo(),它們都定義在<malloc.h>裏。

爲了弄清楚這個問題,我們對上面的例子進行如下的改造:


#include <malloc.h>
void testmap()
{
  malloc_stats();        // <======== 觀察點1
  map<int, float> testmap;
  for (int i = 0; i < 1000000; i++) {
    testmap[i] = (float)i;
  }
  malloc_stats();        // <======== 觀察點2
  testmap.clear();
  malloc_stats();        // <======== 觀察點3
}

這個例子的運行環境是這樣的:

[dengmw@my ~]$ g++ -v
Reading specs from /usr/lib/gcc/x86_64-redhat-linux/3.4.6/specs
Configured with: ../configure --prefix=/usr --mandir=/usr/share/man --infodir=/usr/share/info --enable-shared --enable-threads=posix --disable-checking --with-system-zlib --enable-__cxa_atexit --disable-libunwind-exceptions --enable-java-awt=gtk --host=x86_64-redhat-linux
Thread model: posix
gcc version 3.4.6 20060404 (Red Hat 3.4.6-9)

程序的運行結果是這樣的:

在觀察點1:
*       system bytes     =          0
*       in use bytes     =          0
在觀察點2:
*       system bytes     =          48144384
*       in use bytes     =          48005120
在觀察點3:
*       system bytes     =          48140288    <==== malloc cache the memory here
*       in use bytes     =          5120


很明顯,儘管程序員顯式地調用了map::clear(),但是malloc並沒有把這些內存歸還給OS,而是緩存起來了。所以說,這個例子的罪魁禍首並不是libstdc++的的STL,而是glibc的malloc。



3. 侯捷的《STL源碼剖析》有點過時了
在調試上面的例子的時候,我在看了不少的書籍和網上的文章,其中就包括了侯捷的《STL源碼剖析》,但是這本書已經過時了,因爲他寫這本書的時候,g++的版本才2.9。我把g++的各個版本的源代碼都下載下來了,並且進行了比較,總結如下:
侯捷的《STL源碼剖析》只對於gcc-3.3.*及以前的版本是對的;對於gcc-3.4.*以後的版本,STL中關於內存的代碼變了
當前,大家使用的gcc大都是3.4.6版本或者更加新的版本
gcc-3.3分支從2003-05-13發佈第1版,到2005-05-03發佈3.3.6
gcc-3.3的默認的Allocator,定義在"include/bits/stl_alloc.h"裏,確實是帶有cache的 (即常說的memory pool)
gcc-3.4的默認的Allocator,定義在"include/bits/allocator.h"裏,它的真實的實現是"include/ext/new_allocator.h",這個實現不帶cache,只是new和delete的簡單封裝



4. STL內存管理的基礎知識(gcc-3.4.*及以後的)

通過這次對STL的研究,我學到不不少新的知識。可能這些內容你都已經會了,-.-,我比較弱,下面的內容我是第一次知道的:

STL有很多種allocator,默認採用的是std::allocator,我們沿着這樣的頭文件路線,可以找到它的最終實現:
-> "include/bits/allocator.h"
-> "include/i386-redhat-linux/bits/c++allocator.h"
-> "include/ext/new_allocator.h"(即是說,std::allocator == __gnu_cxx::new_allocator)

根據C++的標準,STL的allocator,把對象的申請和釋放分成了4步:
第1步:申請內存空間,對應函數是allocator::allocate()
第2步:執行構造函數,對應函數是allocator::construct()
第3步:執行析構函數,對應函數是allocator::destroy()
第4步:釋放內存空間,對應函數是allocator::deallocate()
STL崇尚拷貝,你往容器裏放東西或者從容器裏取東西,都是要調用拷貝構造函數的。比如,你有一個對象a要插入到map裏,過程是這樣的:
map先申請一個結點的空間
調用拷貝構造函數初始化該結點
把新結點插入到map的紅黑樹中

STL中實現了好多種不同的更爲具體的allocator,如下(GNU GCC關於Memory的官方文檔):
__gnu_cxx::new_allocator: 簡單地封裝了new和delete操作符,通常就是std::allocator
__gnu_cxx::malloc_allocator: 簡單地封裝了malloc和free函數
__gnu_cxx::array_allocator: 申請一堆內存
__gnu_cxx::debug_allocator: 用於debug
__gnu_cxx::throw_allocator: 用於異常
__gnu_cxx::__pool_alloc: 基於內存池
__gnu_cxx::__mt_alloc: 對多線程環境進行了優化
__gnu_cxx::bitmap_allocator: keep track of the used and unused memory locations.
上面的8個allocator的實現中,bitmap_allocator、pool_allocator和__mt_alloc是基於cache的,其它的不基於cache
* 那麼?如何指定使用一個特殊的allocator呢?示例如下:

map<int, int> a1;                                    // 方法1
map<int, int, less<int>, std::allocator<pair<int, int> > > a3;      // 方法2
// 方法3,方法1、方法2、方法3都是等價的
map<int, int, less<int>, __gnu_cxx::new_allocator<pair<int, int> > > a2; 
// 方法4,使用了基於cache的allocator
map<int, int, less<int>, __gnu_cxx::__pool_alloc<pair<int, int> > >  a4; 




5. 內存碎片是容易被忽視的導致OutOfMemory的原因

這個觀點有點類似於磁盤碎片,也可以稱爲內存碎片吧,當內存碎片過多的時候,極容易出現OutOfMemory錯誤;

使用STL的map特別容易出現這種情況,往map裏插入了海量的小對象,然後釋放了一些,然後再想申請內存時,就出現OutOfMemory錯誤了;

這種現象不只是在使用STL的情況會發現,下面舉一個例子來說明內存碎片的問題,儘管這個例子沒有使用STL。

舉例之前,先說明一下這個例子中使用的兩個查看當前進程的內存統計量的2個函數:
int get_max_malloc_length_inMB() : 得到當前可以申請的最長的內存長度(MB);這個函數不停地調用p=malloc(length*1024*1024);如果成功,則length++,並且free(p);如果失敗,返回(length-1)。
int get_free_mem_inKB() : 得到當前可以申請的內存總量(KB);這個函數不停地調用malloc(1024)來申請1KB的內存;如果成功,把這1KB的內存存起來,並且count++;如果失敗,則把所有的1KB內存釋放,再返回count。
爲了測試方便,我在運行程序前,設置了進程的最大內存爲200MB,使用的命令如下:

ulimit -m 204800;
ulimit -v 204800;

這個例子把申請到的內存以矩陣的形式存儲起來,先按列優先把指針存起來,再按行優先進行free,這樣會造成大量的內存碎片;例子的僞代碼如下:

char ** Ptrs = (char**) malloc( ROW * sizeof(char*) );
...

// 第1步: 佔領所有的內存,按列優先進行申請
for(j=0; j<COL; ++j) {
    for(i=0; i<ROW; ++i) {
        Ptrs[j][i] = malloc(1024);
    }
}

// 第2步:按行優先釋放所有的內存,在中間多次調用get_max_malloc_length_inMB和get_free_mem_inKB來查看內存使用情況
for (i=0; i<ROW; ++i) {
    for (j=0; j<COL; ++j) {
        free( Ptrs[i][j] );
    }
    free(Ptrs[i]);
    // 得到兩個關於內存的統計量
    get_max_malloc_length_inMB();
    get_free_mem_inKB();
}

// 第3步:釋放Ptrs,再獲取一次內存的統計量
free(Ptrs);
get_max_malloc_length_inMB();
get_free_mem_inKB();

需要關注的是,內存的申請的順序是按列優先的,而釋放的順序是按行優先的,這種做法就是模擬內存的碎片。<BR>
運行上面的程序後,得到的結果是:在釋放內存的過程中,max_malloc_length_inMB長期保持在0 MB,當全部釋放完後,max_malloc_length_inMB變成了 193 MB<BR>

max_malloc_length_inMB:
    196 MB -> 0 MB -> 0 MB -> ... -> 0 MB -> 0 MB -> ...
           -> 0 MB -> 0 MB -> 195 MB
free_mem_inKB:
    199374 KB -> 528 KB -> 826 KB -> ... -> 96037 KB -> 96424 KB -> ...
              -> 197828 KB -> 198215 KB -> 198730 KB

上面的結果引申出這樣的結論:
OutOfMemory錯誤,並不一定是內存使用得太多;
當一個程序申請了大量的小內存塊 (比如往std::map中插入海量的小對象),導致內存碎片過多的話,一樣有可能出現OutOfMemory錯誤


6. 一些別的收穫
6.1 libc.so.6和glibc-2.9有什麼不同?
參考文獻:http://en.wikipedia.org/wiki/GNU_C_Library
在80年代,FSF寫了glibc;
後來,linux kernel的人照着glibc,寫了"Linux libc",一直從libc.so.2到libc.so.5
到1997年,FSF發佈了glibc-2.0,這個版本有很多優點,比如支持有更多的標準,更可移植;linux kernel的人就把"Linux libc"的項目砍掉了,重新使用glibc-2.0,然後就命名爲libc.so.6
如果你運行一下這個命令"ls -lh /lib/libc.so.6",你會發現它其實是一個符號鏈接,在我的電腦上,它指向了"/lib/libc-2.9.so"
6.2 申請內存的方式共有多少種?
參考文獻:glibc manual中的第3章(見http://www.gnu.org/software/libc/manual/)
exec
fork
進程內:
global var or static var
local var
malloc()
memory map file
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章