浅谈C++容器效率的优化

作者:岳知涵

在NOIP中,C++的容器的用途非常广泛,但经常因常数过大而超时。怎样才能提高它们的效率呢?我们知道,容器是存储同一类对象的对象,既然“对象”我们无法改变,那么我们只能从“存储”入手,不难想到,不同容器在实现上的根本区别是它们对应着不同的内存组织方式,内存管理无疑是这种实现的核心,所以优化内存管理是加快容器效率的最好途径之一。

一、内存分配器简介

怎样才能优化内存管理呢?很简单,C++为我们提供了这样的接口,我们可以通过自定义容器模板中的最后一个allocator内存分配器来提高容器效率,在默认情况下,alloc为allocator,allocator是C++的用于泛型化内存分配的模板,它在申请小空间时通常很快(比new运算符快一些),又因为自定义一般化的内存分配器(可自行查询“内存池”的资料)需要较大的代码量,而且效率未必高,所以本文我们只讨论某些特殊情况下的内存分配。

二、编写内存分配模板

下面我们来编写一个简单的内存分配器,为了简单起见,我们继承C++的allocator

template
struct myalloc:allocator

构造函数是必不可少的:

myalloc(){}
template//别忘记写模板,下同
myalloc(const myalloc &a){}
template
myalloc& operator=(const myalloc &a)
{
	return *this;
}

由于继承了allocator,所以一定要把rebind覆盖掉,否则other分配器将是allocator,而不是myalloc:
template
template
struct rebind
{
	typedef myalloc other;
};

接下来我们将讨论一下:如何实现T* allocate(size_t n)和void deallocate(T* p,size_t n)成员函数。我们只研究内存分配的最简单情况:不回收内存分配和定长内存分配。

三、不回收内存分配
回想一下链表的写法,在竞赛中最常见的实现往往是用数组模拟链表,存储链表的空间是从一个足够大的数组中得到的,这样编写快、效率也高。于是我们得到了主要思想:化动为静!我们一次性预留足够大的“线性”空间,申请内存时从“线”的开头截掉一部分,记录开头的位置即可。思路很直观,实现也很简单。

开一个足够大的数组space,用space_len记录已经分配到的空间大小,注意space必须是全局变量,如果写在类里的话,那么这个类每次实例化都会重复占用空间,很可能超过内存限制。

char space[10000000];
int space_len=0;

可以写allocate函数了,n表示需要的空间大小,函数返回申请到的空间首地址:

T* allocate(size_t n)
{
	T *result=(T*)(space+space_len);
	space_len+=n*sizeof(T);
	return result;
}

然后把void deallocate(T* p,size_t n){}留空,短短几行代码就完成了一个不回收的内存分配器。

四、定长内存分配

在要申请的内存长度确定的情况下,我们可以完成内存释放函数以节省内存空间。内存释放的根本目的是将释放的内存用于下一次申请,由于我们要的内存是定长的,所以保存一下刚刚释放掉的内存地址,下次申请时直接返回这个地址不就可以了?我们用栈来保存地址:

int stack[10000000],stack_len=0;

释放很简单:

void deallocate(T* p,size_t n)
{
	stack[stack_len++]=(int)p; 
}

再修改一下申请函数:

T* allocate(size_t n)
{
	if(stack_len!=0)
		return (T*)stack[--stack_len];
	else
	{
		T *result=(T*)(space+space_len);
		space_len+=n*sizeof(T);
	return result;
	}
}

五、效率对比

大功告成!分配效率如何呢?这里用list进行效率测试:

int start=clock();
list > L;
for(int i=0;i<800000;++i)
{
	L.push_back(500);
	L.pop_back();
}
cout << "myalloc:" << (double)(clock()-start)/CLOCKS_PER_SEC << endl;

start=clock();
list L2;
for(int i=0;i<800000;++i)
{
	L2.push_back(500);
	L2.pop_back();
}
cout << "allocator:" << (double)(clock()-start)/CLOCKS_PER_SEC << endl;

笔者测试结果:
myalloc:0.063
allocator:0.203
我们的myalloc比默认的allocator快了近三倍!

那么它在实际应用中的效果如何呢?笔者用NOIP2012提高组的drive测试了一下,其中用到了一个list,最后4个点会超时,最慢的一个用时1.33秒,用刚刚介绍的定长内存分配器优化,最慢的一个点仅用时0.62秒。

六、注意事项
上文介绍的内存分配器缺少对内存上限的处理,因为有时我们并不清楚要开多大的space才够,所以当内存不足时通常要进行额外处理,用allocator来分配内存以防止内存泄露是个不错的选择,请读者自己完善代码。
另外有些C++容器允许通过reserve成员函数预留内存,避免不必要的重复申请操作,这也是一个很有效的优化方法。

你可能感兴趣的:(研究,OI/ACM)