1. 程式人生 > >STL容器 記憶體釋放

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原始碼剖析啊。