new不是你想用就能用

C++阵营

       相信绝大多数人选择了C++,原因是可以自由的控制内存的分配与回收,从而去提高程序运行的效率;或者是怀着“设计者”的心情去不断挑战极限。

       对于第一点,先来说明一个名次“托管”,这是由Microsoft的专有概念,主要分为托管代码、托管数据、托管类三大部分,这里主要针对托管数据进行说明。托管数据就是由公共语言运行的垃圾回收器进行分配和释放的数据。即像Java和C#一样,程序员只需要new,而不需要关心delete,统一由系统回收。

       但是系统回收资源,必须要有一定的内存回收机制,并且要定期去执行这个机制,势必会占用一定的系统资源:回收之前,垃圾占用内存;回收时,占用CPU资源。(具体我没有研究,只是个人观点)

       那么C++的内存管理方式就避免了上述的资源浪费,但是会造成一系列的内存Bug,相信大家都深有体会。

       废话有点多,进入主题,既然C++的内存分配有如此好处,那new是不是想用就能用呢?


一个例子

       首先,说明环境,下面全部测试代码在Windows 7平台下,工具为Visual Studio 2013,环境为Release Win32。(已验证过Visual Studio 2010,MinGW,内存分配上大同小异)

        此例子来自于一个同事针对Excel测试时发现,具体如下:

int main(void)
{
   size_t VecSize = 1024 * 1024 * 10;
   std::vector<double *> doublePVec(VecSize, nullptr);
 
   system("PAUSE");
 
   for (size_t i = 0; i < VecSize; ++i)
    {
       double *pDouble = new double;
       doublePVec[i] = pDouble;
    }
 
   system("PAUSE");
 
   std::for_each(doublePVec.begin(), doublePVec.end(), [](double*pDouble){delete pDouble;});
   std::vector<double *>().swap(doublePVec);
 
   system("PAUSE");
   return 0;
}

       打开你的任务管理器,分别记录三次“PAUSE”时内存大小,可以发现大概如下:

       第一次:43088K          = 42M

       第二次:207656K       = 202M

       第三次:3424K            = 3M

       根据第三次的内存为程序占用内存,减去这部分内存,可以发现第一次及第二次内存分别为40M和200M。


问题来了

       根据上面的小例子,如果善于思考的同学,你的问题应该来了:

       Win32平台下,vector的需要申请1024 * 1024 * 10 * 4 = 40M的空间大小,这个是没有问题的,所以第一次内存大小合理。

       第二次内存应该为40M + 1024 * 1024 * 10 *8 =120M的空间大小,但是为什么会多了80M的内存?!

       当时队伍里面几位小伙伴讨论了几种可能方式,例如说内存碎片,内存分配算法等各个方面,但是我感觉都不足以彻底解释这个问题,于是继续进行了下面的尝试。


继续尝试

       尝试的方法就是改变new的大小,大家都知道内存对齐上一般是4字节对齐(针对这一点也有一个很有趣的事,以后再叙述),那么就按照4的倍数来不断改变申请空间大小,看看具体内存占用情况。

       首先,先用int替换double,发现以下结果:

       第一次:43088K          = 42M

       第二次:207656K       = 202M

       第三次:3424K            = 3M

       出现了这个结果,着实震惊了一把,在堆上申请空间,int居然和double是一样的?!

       既然这样,那么把int再换成char,这样直接去申请数组空间,可以方便控制大小,大小先设为12,执行一遍,结果如下:

       第一次:43092K          = 42M

       第二次:289932K       = 283M

       第三次:3980K            = 3M

       内存居然涨到280M了?!这跟预期的40M + 1024 * 1024 * 10 * 12= 160M差了120M,天啊,我怀疑我的测试用例写错了,反复检查了一遍,发现没问题呀。于是继续增加大小,char数组增长到16,结果如下:

       第一次:43092K          = 42M

       第二次:289932K       = 283M

       第三次:3980K            = 3M

       一次次的震惊,已经不能说明我的心情了,后面的结果就是不断增长大小,然后进行测试,就不贴结果了,大家可以自己测试一下。


推导公式

       根据结果,发现了以下规律:

        1. Windows下内存分配最小大小为8Byte。

        2. 每次分配都会增加8Byte的额外空间。

        根据上述两条结果,假设申请空间为xByte,则实际分配的大小N为:

        N = (⌈x/8⌉ + 1) * 8Byte

        所以针对上面double的情况,则申请1024 *1024 * 10的空间为:

        N = (1 + 1) * 8* 1024 * 1024 * 10 = 160M


进一步思考

        第一,Windows的内存分配策略究竟是怎样的?

        由于资料不是很全,只能在网上查询,并根据结果来做大概的推断。首先,Windows下Debug和Release的分配方式是不同的,Debug模式下需要占用大得多的内存来记录更多的信息用于调试;其次,Windows下Debug和Release模式下,new最终都是通过调用HeapAlloc等方法来分配内存,这个可以在调试模式下看见,只是Release模式下到HeapAlloc方法就不能再进行跟踪了;第三,Windows将内存划分为8Byte为基准的各个大小段,例如8Byte,16Byte,24Byte…1024Byte,凡超过1024Byte的会单独记录,1024Byte以下的直接在管理区间内进行划分;第四,Windows下一个进程可能会有多个堆空间;第五,通过第三、四点可以发现,一个进程可能存在内碎片,外碎片只可能存在于多个堆空间之间;第六,每次分配会多8Byte的空间,主要用来记录分配的空间记录用,例如分配空间的上一部分和下一部分的地址,分配的大小,空间是否有效等信息。

        第二,C++中new也不是想用就能用。

        进行大量信息存储时,一定要考虑new时堆空间的开销,32位环境下,每个进程理论可以占用空间大小为2^32,即4G,但是由于操作系统占用最高位,所以实际只能使用2G的空间。如果进行了大量小信息的存储会导致2G空间很快消耗。

        如果要考虑构造函数的开销,那么最佳处理方式就是先申请一块连续空间,然后对象在这块连续空间上产生,即Placement new。

        第三,一定要考虑Debug的场景。

        任何一个人都不能保证一个程序没有Bug,并且随着工程规模不断扩大,Bug也会不断增长,具体数据可以参考《代码大全》,里面叙述的很清楚。那么任何程序必然会有Debug的过程,所以Release下的代码拿到Debug下,可能根本无法调试。这个在项目中已经吃过很大的亏了,要么慢的无法忍受,要么直接崩掉,根本原因是代码写的不够正确,一切都依赖于编译器的优化是不行的。即使一个简简单单的++操作符,也会造成很大的影响。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值