STL中vector的內存分配機制

一些好的公司校園招聘過程中(包括筆試、面試環節),經常會涉及到STL中vector的使用(主要是筆試)及其性能(面試)的分析。今天看了下相關文章,也寫了幾個小的測試程序跑了跑。算是總結下,希望對需要的人有幫助。

關於vector,簡單地講就是一個動態數組,裏面有一個指針指向一片連續的內存空間,當空間不夠裝下數據時會自動申請另一片更大的空間,然後把原有數據拷貝過去,接着釋放原來的那片空間(所以之前引用的vector的地址就失效了,不能訪問了);當釋放或者說是刪除裏面的數據時,其存儲空間並不會釋放,僅僅只是清空了裏面的數據。接下來,我會詳細地說說這些。

備註:本文的相關程序都是在windows 7+VS2008環境下測試。

一、首先,看看vector的內存分配機制:

  1. vector<int> arr;  
  2. ofstream wf("1.txt");  
  3. for(int i=0;i<100;++i)  
  4. {  
  5.     arr.push_back(i);  
  6.     wf<<"capacity="<<arr.capacity()<<",size="<<arr.size()<<end;  
  7. }  
  8. wf.close();  
capacity()返回的是當前vector對象緩衝區(後面的對vector維護的內存空間皆稱爲緩衝區)實際申請的空間大小,而size()返回的是當前對象緩衝區中存儲數據的個數,capacity永遠是大於等於size的,當size和capacity相等時繼續添加數據時vector會擴容。

再來看看1.txt中的數據:

capacity=1,size=1
capacity=2,size=2
capacity=3,size=3
capacity=4,size=4
capacity=6,size=5
capacity=6,size=6
capacity=9,size=7
capacity=9,size=8
capacity=9,size=9
capacity=13,size=10
capacity=13,size=11
capacity=13,size=12
capacity=13,size=13
capacity=19,size=14
capacity=19,size=15
capacity=19,size=16
capacity=19,size=17
capacity=19,size=18
capacity=19,size=19
capacity=28,size=20
capacity=28,size=21
capacity=28,size=22
capacity=28,size=23
capacity=28,size=24
capacity=28,size=25
capacity=28,size=26
capacity=28,size=27
capacity=28,size=28
capacity=42,size=29
capacity=42,size=30
capacity=42,size=31
capacity=42,size=32
capacity=42,size=33
capacity=42,size=34
capacity=42,size=35
capacity=42,size=36
capacity=42,size=37
capacity=42,size=38
capacity=42,size=39
capacity=42,size=40
capacity=42,size=41
capacity=42,size=42
capacity=63,size=43
capacity=63,size=44
capacity=63,size=45
capacity=63,size=46
capacity=63,size=47
capacity=63,size=48
capacity=63,size=49
capacity=63,size=50
capacity=63,size=51
capacity=63,size=52
capacity=63,size=53
capacity=63,size=54
capacity=63,size=55
capacity=63,size=56
capacity=63,size=57
capacity=63,size=58
capacity=63,size=59
capacity=63,size=60
capacity=63,size=61
capacity=63,size=62
capacity=63,size=63
capacity=94,size=64
capacity=94,size=65
capacity=94,size=66
capacity=94,size=67
capacity=94,size=68
capacity=94,size=69
capacity=94,size=70
capacity=94,size=71
capacity=94,size=72
capacity=94,size=73
capacity=94,size=74
capacity=94,size=75
capacity=94,size=76
capacity=94,size=77
capacity=94,size=78
capacity=94,size=79
capacity=94,size=80
capacity=94,size=81
capacity=94,size=82
capacity=94,size=83
capacity=94,size=84
capacity=94,size=85
capacity=94,size=86
capacity=94,size=87
capacity=94,size=88
capacity=94,size=89
capacity=94,size=90
capacity=94,size=91
capacity=94,size=92
capacity=94,size=93
capacity=94,size=94
capacity=141,size=95
capacity=141,size=96
capacity=141,size=97
capacity=141,size=98
capacity=141,size=99
capacity=141,size=100

數據有點多,提煉下就是這樣的:

capacity=1
capacity=2
capacity=3
capacity=4
capacity=6
capacity=9
capacity=13
capacity=19
capacity=28
capacity=42
capacity=63
capacity=94
capacity=141

看出其中的規律沒?對,就是每次擴容都是增加當前空間的50%(第一次除外);(也有人說內存的增量取決於實現,在不同的編譯器中可能就是capacity*2

9+9/2=13;13+13/2=19;19+19/2=28……

其實STL的源碼我們都可以看到的,具體就在你說安裝的編譯器目錄下,例如,我的VS2008是在:安裝目錄\VC\include下面。你也可以在VS中直接選中#include <vector>右鍵打開。當然了,windows上的STL源碼都是P.J. Plauger寫的(PS:很牛B的博士,百度你就知道),大家都說可讀性極差,我也這麼認爲,我們這些菜鳥還是看GCC中的STL源碼吧。

\VC\include\vector中是這樣擴容的:

  1. <span style="white-space:pre">      </span>if (_Count == 0)//這裏進行了判斷,但是什麼都不做,不知道爲什麼???????  
  2.             ;  
  3.         else if (max_size() - size() < _Count)//編譯器可以申請的最大容量也裝不下,拋出異常_THROW(length_error, "vector<T> too long");  
  4.             _Xlen();    // result too long  
  5.         else if (_Capacity < size() + _Count)//當前空間不足,需要擴容  
  6.             {   // not enough room, reallocate  
  7.             _Capacity = max_size() - _Capacity / 2 < _Capacity  
  8.                 ? 0 : _Capacity + _Capacity / 2;    // try to grow by 50%,擴容50%  
  9.             if (_Capacity < size() + _Count)//擴容50%後依然不夠容下,則使容量等於當前數據個數加上新增數據個數  
  10.                 _Capacity = size() + _Count;  
  11.             pointer _Newvec = this->_Alval.allocate(_Capacity);//申請新的空間  
  12.             pointer _Ptr = _Newvec;  
  13.   
  14.             _TRY_BEGIN  
  15.             _Ptr = _Umove(_Myfirst, _VEC_ITER_BASE(_Where),  
  16.                 _Newvec);   // copy prefix  <span style="white-space:pre">  </span>//拷貝原有數據到新的內存中  
  17.             _Ptr = _Ucopy(_First, _Last, _Ptr); // add new stuff<span style="white-space:pre">  </span>//拷貝新增數據到新的內存的後面  
  18.             _Umove(_VEC_ITER_BASE(_Where), _Mylast, _Ptr);  // copy suffix  
  19.             _CATCH_ALL  
  20.             _Destroy(_Newvec, _Ptr);  
  21.             this->_Alval.deallocate(_Newvec, _Capacity);//釋放原來申請的內存  
  22.             _RERAISE;  
  23.             _CATCH_END  
對的,就是每次擴容50%。至於刪除容器中數據的時候,緩衝區大小並不會改變,僅僅只是清楚了其中的數據,只有在析構函數調用的時候vector纔會自動釋放緩衝區。

看看它的析構代碼:

  1. ~vector()  
  2.     {   // destroy the object  
  3.     _Tidy();  
  4.     }  
  1. <span style="white-space:pre">  </span>void _Tidy()  
  2. <span style="white-space:pre">      </span>{<span style="white-space:pre">  </span>// free all storage  
  3. <span style="white-space:pre">      </span>if (_Myfirst != 0)  
  4. <span style="white-space:pre">          </span>{<span style="white-space:pre">  </span>// something to free, destroy and deallocate it  
  5.   
  6.   
  7.  #if _HAS_ITERATOR_DEBUGGING  
  8. <span style="white-space:pre">          </span>this->_Orphan_all();  
  9.  #endif /* _HAS_ITERATOR_DEBUGGING */  
  10.   
  11.   
  12. <span style="white-space:pre">          </span>_Destroy(_Myfirst, _Mylast);//應該是銷燬vector中的每一個元素吧  
  13. <span style="white-space:pre">          </span>this->_Alval.deallocate(_Myfirst, _Myend - _Myfirst);//釋放緩衝區的空間  
  14. <span style="white-space:pre">          </span>}  
  15. <span style="white-space:pre">      </span>_Myfirst = 0, _Mylast = 0, _Myend = 0;//指針全部歸零  
  16. <span style="white-space:pre">      </span>}  
那麼,我們可以在需要的時候強制釋放緩衝區不?

二、如何強制釋放vector的緩衝區:

答案是可以的,既然析構時會釋放空間,那麼我們就可以換個方式調用析構函數。

  1. //  //方法一、  
  1.     vector<int>().swap(arr); //交換後  
  2. //方法二、  
  3. {  
  4.     vector<int> temp;//臨時對象未初始化,其緩衝區大小爲0,沒有數據  本人註釋:vector<int>temp=arr;                      //這樣swap之後arr才能保留原來的元素,並且收縮到合適的大小
  5.     arr.swap(temp);//與我們的對象交換數據,arr的緩衝區就沒了。  
  6. }//臨時變量會被析構,temp調用vector析構函數釋放空間  

swap交換技巧實現內存釋放思想:vector()使用vector的默認構造函數建立臨時vector對象,再在該臨時對象上調用swap成員,swap調用之後對象myvector佔用的空間就等於一個默認構造的對象的大小,臨時對象就具有原來對象v的大小,而該臨時對象隨即就會被析構,從而其佔用的空間也被釋放。代碼如下(http://blog.csdn.net/sunmenggmail/article/details/8605538):

  1. vector< T >().swap(X)  
        下面我們通過一個簡單的示例來show一下:

  1. /******************************************************************* 
  2.  * Copyright (C) Jerry Jiang 
  3.  *                
  4.  * File Name   : swap.cpp 
  5.  * Author      : Jerry Jiang 
  6.  * Create Time : 2012-3-24 4:19:31 
  7.  * Mail        : [email protected] 
  8.  * Blog        : http://blog.csdn.net/jerryjbiao  
  9.  *                
  10.  * Description : 簡單的程序詮釋C++ STL算法系列之十五                   
  11.  *               成員函數swap實現容器的內存釋放    
  12.  *                
  13.  ******************************************************************/  
  14.   
  15. #include <iostream>  
  16. #include <algorithm>  
  17. #include <vector>  
  18. #include <iterator>  
  19.   
  20. using namespace std;  
  21.   
  22. int main ()   
  23. {  
  24.     int x = 10;  
  25.     vector<int> myvector(10000, x);    
  26.   
  27.     //這裏打印僅僅是元素的個數不是內存大小  
  28.     cout << "myvector size:"  
  29.          << myvector.size()  
  30.          << endl;  
  31.   
  32.     //swap交換函數釋放內存:vector<T>().swap(X);  
  33.     //T:int ; myvertor代表X  
  34.     vector<int>().swap(myvector);  
  35.   
  36.     //兩個輸出僅用來表示swap前後的變化  
  37.     cout << "after swap :"  
  38.          << myvector.size()  
  39.          << endl;  
  40.   
  41.     return 0;  

 本人註釋:我發現很多文章可能在轉載的過程中出錯了,因爲這樣swap的話,myvector就被清空了,而不是把內存收縮到合適,原來的元素也沒保留,我通過調試和查看別的文章發現,應該對上述代碼做如下修改:

#include <iostream>  
#include <algorithm>  
#include <vector>  
#include <iterator>  

using namespace std;

int main()
{
	int x = 10;
	vector<int> myvector;
	myvector.push_back(1);
	myvector.push_back(1);
	myvector.push_back(1);
	myvector.push_back(1);
	myvector.push_back(1);
	//打印元素的個數和容量大小 
	cout << "myvector size:"
		<< myvector.size()
		<< endl;
	cout << "myvector capacity:"
		<< myvector.capacity()
		<< endl;
	vector<int>(myvector).swap(myvector);

	//兩個輸出用來表示swap前後的變化  
	cout << "after swap :"
		<< myvector.size() << " "
		<< myvector.capacity()
		<< endl;
	return 0;
}
輸出結果爲:
myvector size:5
myvector capacity:6
after swap:5 5
可以看出,這樣swap之後vector容量收縮到了合適。

vector<type> v;
//.... 這裏添加許多元素給v
//.... 這裏刪除v中的許多元素
vector<type>(v).swap(v);
//此時v的容量已經儘可能的符合其當前包含的元素數量
//對於string則可能像下面這樣
string(s).swap(s);

(添加大括號的修改方法本人在上面已經註釋了,需要先給臨時變量初始化)該方法的工作過程爲:即先創建一個臨時拷貝與原先的vector一致,值得注意的是,此時的拷貝 其容量是儘可能小的符合所需數據的。緊接着將該拷貝與原先的vector v進行 交換。好了此時,執行交換後,臨時變量會被銷燬,內存得到釋放。此時的v即爲原先 的臨時拷貝,而交換後的臨時拷貝則爲容量非常大的vector(不過已經被銷燬)。

參考:http://blog.csdn.net/wzzfeitian/article/details/39300321  http://blog.jobbole.com/37700/ http://www.cnblogs.com/summerRQ/articles/2407974.html


三、如何使用提高性能:

爲了比較,我們用了三種方式來把100個數據存入vector中,分別是:1、直接每次push_back();2、使用resize()提前分配100個空間,然後push_back;3、使用reserve提前分配100個存儲空間。MSDN中,這兩個個函數的說明分別是:

reserve Reserves a minimum length of storage for a vector object, allocating space if necessary.

resize Specifies a new size for a vector.

在這裏我們初始化的時候使用感覺好像是差不多。

  1. clock_t start=clock();  
  2.     for(int num=0;num<10000;++num)  
  3.     {  
  4.         vector<int> v1;  
  5.         for(int i=0;i<100;++i)  
  6.             v1.push_back(i);  
  7.     }  
  8.     cout<<"直接push循環10000次用時:"<<clock()-start<<endl;  
  9.     start=clock();  
  10.     for(int num=0;num<10000;++num)  
  11.     {  
  12.         vector<int> v2;  
  13.         v2.resize(100);  
  14.         for(int i=0;i<100;++i)  
  15.             v2.push_back(i);  
  16.     }  
  17.     cout<<"先resize預設大小再push循環10000次用時:"<<clock()-start<<endl;  
  18.     start=clock();  
  19.     for(int num=0;num<10000;++num)  
  20.     {  
  21.         vector<int> v3;  
  22.         v3.reserve(100);  
  23.         for(int i=0;i<100;++i)  
  24.             v3.push_back(i);  
  25.     }  
  26.     cout<<"先reserve預設大小再push循環10000次用時:"<<clock()-start<<endl;  

結果卻不盡相同


reserve只是保持一個最小的空間大小,而resize則是對緩衝區進行重新分配,裏面涉及到的判斷和內存處理比較多,當然了在這裏由於最初都是空的所以差別不大。

兩者的區別查看:vector::reserve和vector::resize的區別

由此可見,對於數據數目可以確定的時候,先預設空間大小是很有必要的。直接push_back數據頻繁移動很是耗時(當然了,數據小的可以忽略的)。


真個測試程序的完整代碼如下

  1. #include "stdafx.h"  
  2. #include "btree.h"  
  3. #include <vector>  
  4. #include <iostream>  
  5. #include <Windows.h>  
  6. #include <fstream>  
  7. #include <time.h>  
  8. using std::ofstream;  
  9. using std::cout;  
  10. using std::endl;  
  11. using std::vector;  
  12. int _tmain(int argc, _TCHAR* argv[])  
  13. {  
  14.     /************************************************************************/  
  15.     /* vector如何強制釋放內存空間                                         */  
  16.     /* 默認只有析構時纔會釋放                                              */  
  17.     /************************************************************************/  
  18.     vector<int> arr;  
  19.     cout<<"默認情況未初始化時,capacity="<<arr.capacity()<<endl;  
  20.     arr.resize(100,100);  
  21.     arr.reserve(50);  
  22.     arr.resize(50);  
  23.     cout<<"現在,capacity="<<arr.capacity()<<endl;  
  24.     vector<int>::iterator itor=arr.begin()+10;  
  25.     arr.erase(arr.begin(),itor);  
  26.     cout<<"capacity="<<arr.capacity()<<",size="<<arr.size()<<endl;  
  27. //  //方法一、  
  28.     vector<int>().swap(arr); //強制釋放空間  
  29.     //方法二、  
  30.     {  
  31.         vector<int> temp;  
  32.         arr.swap(temp);  
  33.     }//臨時變量會被析構  
  34.     cout<<"capacity="<<arr.capacity()<<",size="<<arr.size()<<endl;  
  35.     clock_t start=clock();  
  36.     for(int num=0;num<10000;++num)  
  37.     {  
  38.         vector<int> v1;  
  39.         for(int i=0;i<100;++i)  
  40.             v1.push_back(i);  
  41.     }  
  42.     cout<<"直接push循環10000次用時:"<<clock()-start<<endl;  
  43.     start=clock();  
  44.     for(int num=0;num<10000;++num)  
  45.     {  
  46.         vector<int> v2;  
  47.         v2.resize(100);  
  48.         for(int i=0;i<100;++i)  
  49.             v2.push_back(i);  
  50.     }  
  51.     cout<<"先resize預設大小再push循環10000次用時:"<<clock()-start<<endl;  
  52.     start=clock();  
  53.     for(int num=0;num<10000;++num)  
  54.     {  
  55.         vector<int> v3;  
  56.         v3.reserve(100);  
  57.         for(int i=0;i<100;++i)  
  58.             v3.push_back(i);  
  59.     }  
  60.     cout<<"先reserve預設大小再push循環10000次用時:"<<clock()-start<<endl;  
  61.     vector<int> v4;  
  62.     ofstream wf("2.txt");  
  63.     int nFlag=v4.capacity();  
  64.     for(int i=0;i<100;++i)  
  65.     {  
  66.         v4.push_back(i);  
  67.         if(nFlag!=v4.capacity())  
  68.         {  
  69.             nFlag=v4.capacity();  
  70.             cout<<"new buffer size="<<nFlag<<endl;  
  71.             wf<<"capacity="<<nFlag<<endl;  
  72.         }  
  73.     }  
  74.     wf.close();  
  75.     cout<<"max_size="<<arr.max_size()<<endl;  
  76.     return 0;  
  77. }  

參考了一些前輩的文章,能力有限,歡迎指教,相互學習。

轉自:http://blog.csdn.net/mfcing/article/details/8746256

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章