STL容器 記憶體釋放
在一個專案中,需要在服務端儲存玩家的錄影回放資料,採用vector/map容器暫存了下發的訊息資料,等待遊戲結束後就將其寫入檔案,然後用clear清除掉這塊快取。
遊戲上線了之後,發現其佔用的記憶體一直上升,搜尋日誌後發現,每局結束後回放佔用的空間並沒有釋放掉,隨著房間一直保留。也就是假設一共1000個房間,每個房間都有玩家遊戲過後,就會有一千份回放空間沒釋放。
瞬 · 間 · 爆 · 炸
普遍說法是vector的clear並沒有真正的釋放記憶體,僅僅只是呼叫了元素的解構函式,然後調整了vector內部存取的位置。實際可以根據以下程式碼來觀察:
#include<vector> using namespace std; struct TestClass { TestClass(){ m_value=3; puts("OK1"); } TestClass(int value){ m_value = value; puts("OK2"); } TestClass(const TestClass &tmp){ this->m_value = tmp.m_value; puts("OK3"); } ~TestClass() { printf("free TestClass , m_value = %d\n",m_value); } int m_value; }; int main() { TestClass testTmp(15); { vector<TestClass> vctTest; vctTest.push_back(testTmp); puts("PUSH OVER"); vctTest.clear(); puts("CLEAR OVER"); printf("capacity = %d\n",vctTest.capacity()); printf("test value beyond of limits: %d\n",vctTest[0].m_value); printf("test value beyond of limits: %d\n",vctTest[1].m_value); } puts("TEST OVER"); return 0; }
其輸出的結果是
大致順序為:
1.呼叫了帶參建構函式,testTmp初始化完成。(輸出OK2)
2.在vctTest容器的push_back時,呼叫拷貝建構函式新生成了一個物件,將其壓入vector。(輸出OK3)
3.在vctTest容器的clear中,對其中的元素呼叫了解構函式。(輸出free TestClass , XXXX)
4.在clear完之後仍然在記憶體越界的邊緣試探:(輸出test value beyond of limits: XXXX)
5.出了定義域範圍,釋放vector所佔空間。(輸出TEST OVER)
6.main函式結束,對testTemp呼叫解構函式,並釋放空間。(輸出free TestClass , XXXX)
從上可以觀察出:clear時,解構函式的確是呼叫了,而vector的真實大小capacity仍然是1。
而對於vector來說:其記憶體結構是整段連續的,每當大小不夠時,就重新申請一塊更大的空間,並將原資料複製過去,最後釋放原空間。而這塊更大的空間大小,根據vector自身策略而定,避免頻繁申請記憶體並且複製資料,導致耗時大量增加。自然,為了減少clear之後再次擴容的消耗,所以vector選擇不釋放這塊記憶體,留待之後重複利用。如果真正想要釋放其記憶體,vector也提供了對應的方法。
然後又到了"網上普遍做法"環節:
1.用swap函式交換一個空vector來達到真正釋放記憶體的目的。
//vctTest.swap(vector<TestClass>());
vector<TestClass>().swap(vctTest);
註釋掉的那種在一般情況下會報錯,因為C++11不允許將臨時變數賦給非const左值引用。(但VS2015可以)
交換完之後,等出了定義域範圍,臨時變數就會被釋放掉。
2.在clear完之後用shrink_to_fit函式對vector進行大小重調。
vctTest.shrink_to_fit();
不過需要C++11的支援。按照文件的說明,只是請求釋放記憶體,標準庫並不保證退換記憶體。
最終採用的是第一種方案,測試了一下記憶體的消耗確實穩定了,遊戲結束後相關記憶體會釋放掉,不會一直處於上漲的狀態。
然鵝,事情並沒有結束。因為執行一段時間後,發現記憶體雖然沒有一直漲,但也保持在了一個比較高的消耗上。觀察了一段時間的記憶體佔用,發現每次遊戲開始記憶體上漲,但遊戲結束後並沒有全部釋放。反覆N次之後,記憶體佔用上升到一定值不再增漲,但也幾乎看不出波動。於是推測應該是記憶體被什麼東西快取導致的。
一番查詢後,發現原因在於之前所說的swap操作對map容器並沒有完全起作用。通過小程式測試,往map中塞了40MB的資料後,再進行釋放,其仍然佔用了10MB的記憶體空間。這裡的釋放,是指用swap操作,或者是已執行到臨時變數map的定義域之外。兩種釋放最終結果都是一樣的,均有記憶體空間未被釋放。(當然,不同機器不同情況下未釋放記憶體比例可能不同,但應該都存在未釋放的)
我的結論是map容器會根據策略留下一定比例的記憶體留待之後使用,且這塊記憶體是無法通過swap或者析構map來釋放的。而在之後的測試中,也發現在釋放舊map後仍佔用10MB的情況下,再去向新map中塞小於10MB的資料時,其並不會再去申請新的記憶體空間,而是直接使用了這10MB空間(無論新舊兩個map是否同一結構,記憶體都是可以重複利用的)。
目前來說,暫時還未找到釋放的方法,先考慮調整不用map結構,或者是調整map中儲存的元素結構,縮小其記憶體佔用。
(不過上面這位網友的做法說不定是一個途徑。。?)
另外有一個發現是,即使是vector,如果是巢狀結構(即vector < vector<int> >這種結構),對其進行swap操作也是無法釋放其全部記憶體佔用的,無論是直接整個vector swap,或者遍歷vector內部對其子vector逐個swap後再將總vector進行swap,都是一樣的結果。
測試用程式碼:
#include <stdio.h>
#include <map>
#include <vector>
#include <string.h>
#include <stdlib.h>
#include <windows.h>
using namespace std;
#define MAXLEN 3072
// 定義資料流結構
struct MsgInfo
{
char message[MAXLEN];
MsgInfo() {}
};
// 桌子結構
struct Table
{
map<int,int> m_mapInitXY;
vector< vector<MsgInfo> > m_vtInitMsg;
// 通過swap釋放記憶體
void Clear()
{
map<int,int>().swap(m_mapInitXY);
for (int i = 0; i < m_vtInitMsg.size(); i++)
{
vector<MsgInfo>().swap(m_vtInitMsg[i]);
}
vector< vector<MsgInfo> >().swap(m_vtInitMsg);
}
};
int main()
{
srand(GetTickCount());
map<int,bool> m_tableused;
vector<int> m_tableList;
vector<int> m_tableMsgCount;
// 一共測試訊息條數
int circleTimes = 10000000;
// 當前桌子使用數
int tableCount = 0;
// 結束標誌
int XYEnd = 333;
// 修改持續次數
int changeTimes = 0;
// 期望桌子使用數
int maxTableUsed = 3;
// 桌子上限 (順便用作其他一些上限值)
int tableLimit = 1000;
Table *pTables = new Table[tableLimit];
time_t lasttime = time(NULL);
while(circleTimes--)
{
// 每100000條訊息可修改一次期望桌子使用數
// changetimes代表後N次修改均使用當前值
if (circleTimes % 100000 == 0)
{
if (changeTimes == 0)
{
printf("cost time = %d\n", time(NULL) - lasttime);
printf("Input MaxTableUse ChangeTimes:");
scanf("%d %d",&maxTableUsed,&changeTimes);
lasttime = time(NULL);
}
changeTimes--;
}
// 隨機獲取將要使用的桌子
int tableid = 0;
int listpos = 0;
if (tableCount >= maxTableUsed)
{
listpos = rand() % tableCount;
tableid = m_tableList[listpos];
}
else
{
tableid = rand() % tableLimit;
}
// 新桌子
if (m_tableused[tableid] == false)
{
m_tableused[tableid] = true;
tableCount++;
m_tableList.push_back(tableid);
m_tableMsgCount.push_back(0);
listpos = m_tableList.size() - 1;
printf("CreateTable : %d\n",m_tableList[listpos]);
}
else
{
for (int i = 0; i < m_tableList.size(); i++)
{
if (m_tableList[i] == tableid)
{
listpos = i;
}
}
}
// 儲存資料
Table &tableNow = pTables[tableid];
// 隨機協議號
int XYID = rand() % tableLimit;
MsgInfo infoTemp;
if (tableNow.m_mapInitXY.find(XYID) == tableNow.m_mapInitXY.end())
{
tableNow.m_mapInitXY[XYID] = tableNow.m_vtInitMsg.size();
tableNow.m_vtInitMsg.push_back(vector<MsgInfo>());
}
int XYPos = tableNow.m_mapInitXY[XYID];
tableNow.m_vtInitMsg[XYPos].push_back(infoTemp);
m_tableMsgCount[listpos]++;
// 訊息數達到一定值後或者遇到結束標誌,清除資料
if (m_tableMsgCount[listpos] > tableLimit || XYID == XYEnd)
{
tableNow.Clear();
printf("FreeTable : %d\n",m_tableList[listpos]);
tableCount--;
m_tableused[tableid] = false;
m_tableMsgCount.erase(m_tableMsgCount.begin() + listpos);
m_tableList.erase(m_tableList.begin() + listpos);
}
}
puts("End!");
getchar();
}
/*
sample Input:
50 2
40 2
30 2
20 2
10 2
1 2
*/
果然還是得看看STL原始碼剖析啊。