redhat9i 发表于 2022-10-26 09:39

Unity性能优化:性能优化之内存篇

前言

本文和传统的内存优化不一样,不是讲如何降低内存占用,而是讲编程开发中要注意的内存问题以及一些内存技术的演变与原理。
对惹,这里有一个游戏开发交流小组,希望大家可以点击进来一起交流一下开发经验呀!
本文很长,目录如下:


1: Application进程的内存分段
应用程序的内存分为: 代码段, 数据段, 栈, 堆。
代码段:


数据段:


栈:


栈有多大呢?其实栈的内存大小是相对比较小的,一般是由编译器生成执行文件的时候指定的,这个编程发布者可以改的,你在发布代码的时候,可以给编译器设置栈大小。一般栈默认是几百k, 有时候为了应对特殊的情况,配置编译器,把栈大小调整到1M甚至更大。栈溢出, 当栈的使用超过大小的额定范围,OS会抛出异常”栈溢出”,然后杀掉溢出的进程。比如一个算法是用递归写的,由于递归的层次比较深,导致栈溢出,这时候我们可以减少栈的内存使用或调整编译器来配置更大的栈。如图1.2 vs 编译C++时栈的配置:


堆:当我们希望用到内存时就分配,不用时释放, 内存的生命周期和函数调用无关,完全由开发者自己决定,也不像全局变量一样,运行后内存一直占用,直到进程退出,所以OS给我们提供了一个动态内存分配机制,它提供一个系统调用malloc来分配特定大小的内存,提供一个接口free来释放这块内存,把内存还给OS, OS会划一块区域出来专门用来动态内存分配,叫做堆。
2: OS动态内存分配与手动内存管理
OS系统调用malloc/free的原理:


手动内存管理就是由程序员自己控制,当需要内存的时候调用malloc,不用的时候free。这种的缺点就是程序员容易忘记free。


3:什么是内存碎片,避免内存碎片常用手段


具体分析一下内存碎片在实际中的产生过程,如图1.3为一个线性的虚拟内存空间,




经过进程运行,一段时间后,大量的分配与释放,重新分配,有无数的”18字节”出来了,这就是内存碎片。内存碎片产生的原因其实就是系统反复大量的分配和释放大小不一的内存,导致进程要内存,系统有内存空间,但是这些空间不连续,大小满足不了申请要求,导致分配失败。
搞清楚内存碎片产生的原因,如何避免呢?大量的大小不一的内存块的申请与释放请求导致了碎片,那么解决方案就出来:


4:什么是内存泄漏,预防与追踪内存泄漏的常用方法
内存泄漏:


内存泄漏会不会影响其它的进程任务运行?


内存泄漏的危害: 导致有内存泄漏的进程可用内存越来月少,最后导致进程无法分配内存而停止运行。
追踪内存泄漏的工具: 从系统级别给malloc与free函数,加入调试信息,运行时收集,找出来哪些地方的内存可能没有free,提示给开发者。比如Valgrind工具等,xcode也自动这种追踪工具,原理都一样。
预防内存泄漏:


5: GC自动回收的实现原理与如何避免GC峰值冲击
基于自动垃圾回收的机制是如何实现?
这里给大家分享一种基于引用计数的垃圾回收机制的原理与实现,让你对垃圾回收器有一个全面的了解,其它的垃圾回收机制大同小异,只是判断”垃圾”的方式不一样而已。


实现一个基类我们叫做Object(现在你知道为什么很多垃圾回收的对象基类都是Object了吧,没有错,就是干这个事情的),
class Object {
int refCount; //为0表示为垃圾,增加引用,计数+1,减少引用,计数-1

重载operator=() {改变引用计数}
};以后所有的自动回收的对象都要继承自这个Object。
接下来重载赋值操作符operator=, 当把一个变量赋值为新的Object的时候,把原来的Object引用计数减1,把新的Object的引用计数加1。
定义一个全局的内存分配器负责对象的内存分配与释放,我们暂时叫它Allocator,并提供接口 New,所有的Object的对象创建都基于这个全局的分配器来创建的,同时每分配一个对象出去,内存分配器保存好这个对象的地址或引用。当我们把对象赋值给变量的时候,老对象与新对象会触发引用计数的减加。比如:var a = xxx; a = null, 把a原来的老对象xxx引用计数-1。当没有变量指向xxx,那么它就会被定义为垃圾。
接下来就是给Allocator编写一个垃圾回收的接口叫GarbageCollection,它遍历Allocator记录的所有的分配出去的基于Object的对象,看看这些Object的引用计数是不是为0,如果是,就是垃圾,进而调用OS的释放函数回收内存,把垃圾对象回收。
编写了回收接口后什么时候调用呢?我们一般提供两种方式:手动强制回收垃圾(程序员直接调用)与特定时期的系统自动回收(系统检测,到达一些阈值条件后,调用Allocator的垃圾回收接口)。一般使用系统自动回收,这种还有一个好处,就是前面的垃圾对象还可以反复的再分配出去,比如已经是垃圾的同一类型物体,没有还给OS,但是下次分配又可以直接分配出去,可以避免内存碎片。(这样程序员只管new 对象,有家长帮你管对象了)
如何避免GC峰值冲击?
GC峰值: 某一瞬间,大量的物体的释放,导致GC花了很多时间来计算与回收垃圾,从而导致GC占用了CPU,卡住了程序。
如何避免GC峰值:可以通过平滑掉回收来避免在性能的关键时刻长时间触发GC。
举个例子,比如一个场景,有满屏的敌人,某个玩家放了一个技能,让大量的敌人瞬间全部死亡,如果在清屏的时候,触发了GC,回收大量的”敌人垃圾”,导致游戏长时间卡在了GC上,到导致帧率下降,这个就属于GC卡顿。假设GC 1万个对象卡了3秒, 我们就可以通过调节参数与阈值来避免对这1万个对象一次做回收,而是把1万个对象平滑到一个相对比较长的时间段去回收。每次只回收一部分,分多次回收。这样避免GC峰值带来的性能冲击。具体可以通过问题分析来做出相应的解决方案。
本文到此结束,很长,希望大家有所收获,关注我,一起来讨论内存相关的技术问题。
页: [1]
查看完整版本: Unity性能优化:性能优化之内存篇