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可以)

具体解释请看Virtual_Func博主的这篇文章。

交换完之后,等出了定义域范围,临时变量就会被释放掉。

2.在clear完之后用shrink_to_fit函数对vector进行大小重调。

vctTest.shrink_to_fit();

不过需要C++11的支持。按照文档的说明,只是请求释放内存,标准库并不保证退换内存。

最终采用的是第一种方案,测试了一下内存的消耗确实稳定了,游戏结束后相关内存会释放掉,不会一直处于上涨的状态。


然鹅,事情并没有结束。因为运行一段时间后,发现内存虽然没有一直涨,但也保持在了一个比较高的消耗上。观察了一段时间的内存占用,发现每次游戏开始内存上涨,但游戏结束后并没有全部释放。反复N次之后,内存占用上升到一定值不再增涨,但也几乎看不出波动。于是推测应该是内存被什么东西缓存导致的。

一番查找后,发现原因在于之前所说的swap操作对map容器并没有完全起作用。通过小程序测试,往map中塞了40MB的数据后,再进行释放,其仍然占用了10MB的内存空间。这里的释放,是指用swap操作,或者是已运行到临时变量map的定义域之外。两种释放最终结果都是一样的,均有内存空间未被释放。(当然,不同机器不同情况下未释放内存比例可能不同,但应该都存在未释放的)

再联系网上的解释:STL 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];
	while(circleTimes--)
	{
		// 每100000条消息可修改一次期望桌子使用数
		// changetimes代表后N次修改均使用当前值 
		if (circleTimes % 100000 == 0)
		{
			if (changeTimes == 0)
			{
				printf("Input MaxTableUse ChangeTimes:");
				scanf("%d %d",&maxTableUsed,&changeTimes);
			}
			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]);
		}
		
		// 保存数据 
		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源码剖析啊。

猜你喜欢

转载自blog.csdn.net/Vis_Stu/article/details/80520083
今日推荐