vector 避免記憶體頻繁分配釋放與手動釋放vector記憶體

cws1214發表於2015-08-25

1.避免頻繁重分配

關於STL容器,最令人稱讚的特性之一就是是隻要不超過它們的最大大小,它們就可以自動增長到足以容納你放進去的資料。(要知道這個最大值,只要呼叫名叫max_size的成員函式。)

 

對於vector和string,如果需要更多空間,就以類似realloc的思想來增長大小。這個類似於realloc的操作有四個部分:

  1. 分配新的記憶體塊,它有容器目前容量的倍數。在大部分實現中,vector和string的容量每次以2為因數增長。也就是說,當容器必須擴充套件時,它們的容量每次翻倍。
  2. 把所有元素從容器的舊記憶體拷貝到它的新記憶體。
  3. 銷燬舊記憶體中的物件。
  4. 回收舊記憶體。

給了所有的分配,回收,拷貝和析構,你就應該知道那些步驟都很昂貴。當然,你不會想要比必須的更為頻繁地執行它們。如果這沒有給你打擊,那麼也許當你想到每次這些步驟發生時,所有指向vector或string中的迭代器、指標和引用都會失效時,它會給你打擊的。這意味著簡單地把一個元素插入vector或string的動作需要更新其他使用了指向vector或string中的迭代器、指標或引用的資料結構。

reserve成員函式允許你最小化必須進行的重新分配的次數,因而可以避免真分配的開銷和迭代器/指標/引用失效。但在我解釋reserve為什麼可以那麼做之前,讓我簡要介紹有時候令人困惑的四個相關成員函式。在標準容器中,只有vector和string提供了所有這些函式。

  • size()告訴你容器中有多少元素。它沒有告訴你容器為它容納的元素分配了多少記憶體。
  • capacity()告訴你容器在它已經分配的記憶體中可以容納多少元素。那是容器在那塊記憶體中總共可以容納多少元素,而不是還可以容納多少元素。如果你想知道一個vector或string中有多少沒有被佔用的記憶體,你必須從capacity()中減去size()。如果size和capacity返回同樣的值,容器中就沒有剩餘空間了,而下一次插入(通過insert或push_back等)會引發上面的重新分配步驟。
  • resize(Container::size_type n)強制把容器改為容納n個元素。呼叫resize之後,size將會返回n。如果n小於當前大小,容器尾部的元素會被銷燬。如果n大於當前大小,新預設構造的元素會新增到容器尾部。如果n大於當前容量,在元素加入之前會發生重新分配。
  • reserve(Container::size_type n)強制容器把它的容量改為至少n,提供的n不小於當前大小。這一般強迫進行一次重新分配,因為容量需要增加。(如果n小於當前容量,vector忽略它,這個呼叫什麼都不做,string可能把它的容量減少為size()和n中大的數,但string的大小沒有改變。在我的經驗中,使用reserve來從一個string中修整多餘容量一般不如使用“交換技巧”,那是條款17的主題。)[1]

這個簡介表示了只要有元素需要插入而且容器的容量不足時就會發生重新分配(包括它們維護的原始記憶體分配和回收,物件的拷貝和析構和迭代器、指標和引用的失效)。所以,避免重新分配的關鍵是使用reserve儘快把容器的容量設定為足夠大,最好在容器被構造之後立刻進行。

例如,假定你想建立一個容納1-1000值的vector<int>。沒有使用reserve,你可以像這樣來做:

vector<int> v; for (int i = 1; i <= 1000; ++i) v.push_back(i);

在大多數STL實現中,這段程式碼在迴圈過程中將會導致2到10次重新分配。(10這個數沒什麼奇怪的。記住vector在重新分配發生時一般把容量翻倍,而1000約等於210。)

把程式碼改為使用reserve,我們得到這個:

vector<int> v; v.reserve(1000); for (int i = 1; i <= 1000; ++i) v.push_back(i);

這在迴圈中不會發生重新分配。

在大小和容量之間的關係讓我們可以預言什麼時候插入將引起vector或string執行重新分配,而且,可以預言什麼時候插入會使指向容器中的迭代器、指標和引用失效。例如,給出這段程式碼,

string s; ... if (s.size() < s.capacity()) { s.push_back('x'); }

push_back的呼叫不會使指向這個string中的迭代器、指標或引用失效,因為string的容量保證大於它的大小。如果不是執行push_back,程式碼在string的任意位置進行一個insert,我們仍然可以保證在插入期間沒有發生重新分配,但是,與伴隨string插入時迭代器失效的一般規則一致,所有從插入位置到string結尾的迭代器/指標/引用將失效。

回到本條款的主旨,通常有兩情況使用reserve來避免不必要的重新分配。第一個可用的情況是當你確切或者大約知道有多少元素將最後出現在容器中。那樣的話,就像上面的vector程式碼,你只是提前reserve適當數量的空間。第二種情況是保留你可能需要的最大的空間,然後,一旦你新增完全部資料,修整掉任何多餘的容量。



2.釋放vector記憶體

博主採用 Vector儲存一些資料,但是發現在執行 clear() 之後記憶體並沒有釋放,於是懷疑產生了記憶體洩露。隨後有人回覆:

“vector 的 clear 不影響 capacity , 你應該 swap 一個空的 vector。”

開始並不知道回覆者在說什麼,於是在谷歌上搜尋 vector swap clear 發現已經有類似的問題出現了,並且給出了一些解決方案。

原來這樣的問題在 《Effective STL》中的“條款17”已經指出了

當vector、string大量插入資料後,即使刪除了大量資料(或者全部都刪除,即clear) 並沒有改變容器的容量(capacity),所以仍然會佔用著記憶體。 為了避免這種情況,我們應該想辦法改變容器的容量使之儘可能小的符合當前 資料所需(shrink to fit)

《Effective STL》給出的解決方案是:

  1. vector<type> v; 
  2. //.... 這裡新增許多元素給v 
  3. //.... 這裡刪除v中的許多元素 
  4. vector<type>(v).swap(v); 
  5. //此時v的容量已經儘可能的符合其當前包含的元素數量 
  6. //對於string則可能像下面這樣 
  7. string(s).swap(s); 

即先建立一個臨時拷貝與原先的vector一致,值得注意的是,此時的拷貝 其容量是儘可能小的符合所需資料的。緊接著將該拷貝與原先的vector v進行 交換。好了此時,執行交換後,臨時變數會被銷燬,記憶體得到釋放。此時的v即為原先 的臨時拷貝,而交換後的臨時拷貝則為容量非常大的vector(不過已經被銷燬)

為了證明這一點,我寫了一個程式,如下:

  1. #include <iostream> 
  2. #include <vector> 
  3.   
  4. using namespace std; 
  5.   
  6. vector <string> v; 
  7. char ch; 
  8.   
  9. int main () 
  10.   
  11.     for(int i=0; i<1000000; i++) 
  12.         v.push_back("abcdefghijklmn"); 
  13.     cin >> ch; 
  14.     // 此時檢查記憶體情況 佔用54M 
  15.   
  16.     v.clear(); 
  17.     cin >> ch; 
  18.     // 此時再次檢查, 仍然佔用54M 
  19.   
  20.     cout << "Vector 的 容量為" << v.capacity() << endl; 
  21.     // 此時容量為 1048576 
  22.   
  23.     vector<string>(v).swap(v); 
  24.   
  25.     cout << "Vector 的 容量為" << v.capacity() << endl; 
  26.     // 此時容量為0 
  27.     cin >> ch; 
  28.     // 檢查記憶體,釋放了 10M+ 即為資料記憶體 
  29.     return 0; 

總結

從這個事情說明,自己對STL的瞭解還非常的不夠 平時對vector的清除都懂得采用 clear 方法。

另一方面 對於STL的設計思想也有些瞭解,在建立一個vector後 vector的實際容量一般會比給資料要大,這樣做應該是避免過多的 重新分配記憶體吧。

當然,上面這種方法雖然釋放了記憶體,但是同時也增加了拷貝資料的時間消耗。 不過一般需要重新調整容量的情況都是 vector本身元素較少的情況,所以 時間消耗可以忽略不計。

因此建議以後大家都將呼叫 clear() 改為 swap() 吧。

相關文章