内存使用技巧及内存池实现(一)

        本文只是展示了一些基本的内存管理技巧,处于篇幅没有更深入的讲解,有兴趣可回复一起探讨^_^

        在当前的软件开发环境下,主要分为两大类:客户端和服务端。软件部署在客户端的情况逐渐被Web应用和服务端的网络应用所替代(游戏客户端例外),并且随着硬件的不断升级和成本的降低,各种计算资源和存储资源被程序随意使用,基本不用考虑一个进程多占了几个Byte,多消耗了CPU几个毫秒。不过,某些场合下,比如嵌入式环境和大型服务器(尤其是分布式和云计算平台下的大规模数据和海量计算),对资源的使用仍旧需要在时间和空间上进行优化。所以,作者认为,通过技巧和算法来优化程序还是很有必要的。 

         本文主要介绍一些内存的使用技巧,系统场景为Linux,Windows/Mac也可作为参考。

         1、内存的基本操作 : 

          C语言中如果要申请堆内存,可以通过malloc/calloc/realloc来获得,参数就是内存大小和重新分配的大小。但需要注意的是malloc出来分配的内存是未经过初始化的,不能直接使用,所以要bzero或memset(0),可以用calloc(1,SIZE)一行来代替代替。这里用到的sizeof要注意,获取一个struct或者class的大小一定要sizeof,否则会发生莫名的错误(后面会讲到字节对齐)。

void foo()
{
    // use malloc
    int *p = (int*)malloc(sizeof(int));
    memset(p,0,sizeof(*p));

    // use calloc
    int *c = (int*)calloc(1,sizeof(int));
}

          在C++里通过new来分配堆内存,new除了像malloc一样分配一块内存,还有一个功能就是会调用该类的构造函数,对此对象进行初始化操作,所以如果是C++里,不是申请固定大小的内存自己规划用的只是为对象分配空间的话,就尽量用new。新的C++标准规定,当内存不够,new不成功的时候,不是像以前一样返回NULL,而是抛出一个异常 -- std::bad_alloc,很多项目里,包括Google的开源项目,都不建议使用异常(因为没有finally,并且速度很慢),所以new的时候应该加上std::nothrow。

void foo()
{
    MySample *p = new (std::nothrow) MySamle();
    if (NULL == p)
        DealError();      // handle the error
}

       如果不使用了则用free/delete来释放,并且一定要把原来的指针指NULL。(不指NULL不一定有问题,但是为保险起见)。


       2、Linux进程内存区域分布:


      各个区段的意义和存储的数据网上资料很多,就不在此一一说明了。补充一句,栈是由高地址向低地址延伸,堆反过来(记不住的话可以记住栈分配int空间时候是eps指针-4)。还有堆栈中间不是完全空闲的,最中间一段是mmap(内存映射)使用的。如果想查看大小,可以用size命令,比如: size ./myprogram


      3、malloc的具体平台实现 :

        我们都知道,malloc是libc的标准函数,是c语言的标准。所以windows、mac、linux才都会有这个函数。因为是标准,所以就不能特化,不能特化就意味着某种程度上的速度慢(不是绝对,但是个规则)。malloc是用户态函数,要想从Linux系统中得到实际内存,是要调用linux的brk系统调用的,这个brk就是移动堆顶指针的函数,是将进程的mm_stat机构中的brk值扩大以获得空间。

        malloc在分配内存的时候,参数size不是传入多大就分配多大,试想一个,malloc(1)如果直接分配一个字节,那么malloc的管理字节就会比数据空间大很多(关于管理字节,可以认为存储的是已分配的空间的大小,比如free的时候,并不用传入指针指向空间的大小,因为有管理字节)。还有,cpu通过内存控制器访问内存的时候,是按cpu"喜欢"的对齐方式访问,一般是按4的整数字节读取,如果分配的空间是4的整数倍,就会加快访问。而且malloc会对传入的大小数字进行“归一化”,按照内核的递增序列分配内存(一般最低层次是8byte,按2的幂增长,最大1M,也就是说如果申请大于1M,则要多少给多少)。

      malloc在分配了大量的内存之后,会变得越来越慢,因为malloc的分配过程是现在内存管理模块的"空闲链表"里找到一个合适大小的内存返回,如果空闲链表太长,势必影响速度。


      4、锁住物理内存,不被swap :

      有的应用,如memcached等缓存系统,或者实时性很高的系统,要求分配的内存要全部Hold在内存中,不被swap到磁盘上(Linux系统内存满了才会swap,但需要考虑PageCache)。所以,可以使用mlockall/mlock函数把已分配的内存,甚至以后malloc的内存都一直留在磁盘里。(不好之处是内存满了malloc直接返回NULL,还会触发SIGSEGV信号)。

void lock_mem(void* area, size_t len)
{
    // lock an area of pointer
    mlock(area,len);

    // lock all memory has allocated , but no effect with future
    mlockall(MCL_CURRENT);

    // lock all memory has allocated and future
    mlockall(MCL_CURRENT | MCL_FUTURE);
}

      5、巧用struct/class字节对齐,压缩空间 :

      如果有一个struct/class在内存中可能有10万、或者100万个instanse(大规模服务器经常的情况),可以考虑对它通过字节对齐进行压缩:

#include <stdio.h>

struct NoAlign {
    int a;  
    char b; 
    int c;  
    short d;
    int e;  
    char f; 
};

struct Align { 
    char b; 
    char f; 
    short d;
    int a;  
    int c;  
    int e;  
};

int main()
{
    printf("%d|%d",sizeof(struct NoAlign),sizeof(struct Align));
    return 0;
}
       程序会输出"24|16",我的是x64的系统,可以看出通过适当的调整字段的顺序,可进行字节对齐的压缩,最简单廉价的方法,何乐而不为呢!如果是数组,建议放在最后,这样offset会快些(意义不是很大)。

      6、内存读取访问最好按4的整倍数步进:

      可以看看memset的代码实现,并不是一个for循环然后set每一个字节,这样cpu效率低。memset的实现是按4个字节set一次进行步进,这样效率高些,相对循环次数也多,然后针对剩下的1~3次可用1~3行冗余代码搞定(类似上篇文章介绍的冗余代码的一些好处)。


     7、offsetof()函数可以获得某个字段在struct中的偏移量 :

     offsetof()在32位系统下的实现类似:

#define OFF_SET_OF(s,m) (size_t)&(((s *)0)->m)
      就是将0x00000000转换为struct*指针,然后对齐求其m元素的偏移再转换成地址再转换成数字。


      8、malloc的替代品 : tcmalloc/jemalloc

      介绍了这么多内存管理细节和技巧,总结一下,其实malloc并不是用于大量内存分配操作(容易产生碎片、速度有问题),并且在多线程环境下也不太适合(malloc是不可重入但是线程安全的函数),说他不适合是因为多线程情况下malloc容易泄漏资源。

      这里提出两个解决方式,第一个就是写一个内存池,自己托管内存的使用和分配释放,内存使用技巧及内存池实现(二)将进行详细介绍。还有一种就是使用改良的类malloc分配器,使用google的tcmalloc和jemalloc,tcmalloc在效率上比malloc快了很多(malloc()一次大概300ns,而tcmalloc()大约50ns)。主要是因为TCMalloc减少了多线程程序中的锁争用情况。对于小对象,几乎已经达到了零争用。对于大对象,TCMalloc尝试使用粒度较好和有效的自旋锁。Redis也该用jemalloc来解决内存碎片问题,并且jemalloc在realloc函数上也下了很多功夫,使得realloc原地更新分配,而不是另外开辟一段新空间。

       在编译mysql时候就可以指定tcmalloc,有些资料显示使用tcmalloc的程序有了很大的性能提升(本人未测试)。

       使用tcmalloc很简单,只需要加入脚本 :LD_PRELOAD="/usr/local/lib/libtcmalloc.so"即可。

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
在Unity中实现内存优化可以提高游戏的性能和稳定性。以下是一些常见的内存优化技巧: 1. 使用对象池:对象池是一种重复使用对象的技术,可以避免频繁地创建和销毁对象,从而减少内存分配和垃圾回收的开销。 2. 减少资源加载:尽量避免在运行时频繁加载资源,可以通过预加载、异步加载、动态加载等方式优化资源管理。 3. 优化纹理使用使用合适的压缩格式和分辨率来减小纹理的内存占用,避免同时加载过多的高分辨率纹理。 4. 减少不必要的引用:及时释放不再使用的对象和资源,并确保没有循环引用导致无法释放的内存泄漏。 5. 使用内存优化工具:Unity提供了一些内存优化工具和分析器,如Profiler和Memory Profiler,可以帮助你检测和解决内存泄漏和性能问题。 6. 避免频繁的实例化和销毁:尽量重用对象,避免频繁地实例化和销毁大量对象,可以使用对象池或者对象复用技术。 7. 限制使用动态内存分配:尽量避免频繁地使用动态内存分配函数(如new和malloc),可以使用对象池或者预分配内存的方式来减少动态内存分配的次数。 8. 优化代码逻辑:优化算法和代码逻辑,减少不必要的计算和内存消耗。 这些只是一些常见的内存优化技巧,具体的优化策略还需要根据具体的项目需求和情况来进行调整和实施。同时,使用性能分析工具和测试工具来评估和验证优化效果也是很重要的。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值