Unity的GC优化原理及实践_unity 增量 gc-程序员宅基地

技术标签: unity  游戏引擎  

1.概述

1.1 简介

内存管理一直都是一个让人比较头疼的东西,尤其是现在重度游戏越来越多,每一次卡顿、每一次内存增长对玩家来说都是一个比较差的体验。技术群里总是有人调侃,游戏开发久了人就会变成“GC怪”。事实上,在游戏开发过程中,随着功能的不停迭代,内存问题一直都不能松懈。 Unity 2018集成了正式版的 .NET 4.x 和 C#7.3 ,引入了ref return和ref locals,让值类型操作更加高效,UnsafeUtility让Unsafe编程和Native Memory操作更加方便。Unity 2019加入了增量式GC,减少了GC带来的卡顿问题。目前来说,虽然内存管理还是一个需要注意的问题,但是却比以往版本灵活易用了很多。 本文希望可以从原理出发,以逐步递进的方式讲解GC优化的问题,主要关注于逻辑代码方面,希望可以给大家带来一定的参考价值。

1.2 什么是GC

GC的全称是Garbage Collection,也就是垃圾回收,是一种自动管理堆内存的机制,管理堆内存上对象的分配和释放。 一般来说,程序比较常用的有三种内存管理方式。 第一种是手动管理,即像C/C++一样使用malloc/free或者new/delete来为对象分配释放内存。这种管理方法的优点就是速度快,没有任何额外的开销,缺点是必须追踪每一个对象的使用情况,很容易发生各种问题,比如内存泄漏、野指针和空悬指针等。 第二种方法是使用引用计数(Reference Count)。它的思想是对象创建出来后,维护一个针对该对象的计数,使用该对象的地方对该计数加1,使用完毕后再减1,当计数为0时,销毁该对象。这种方法可以看做是一种半自动的内存管理方式,优点是可以把分配和释放的开销分布在实际使用过程当中,速度也比较快,不过会存在一个循环引用的问题。引用计数是一种比较常用的内存管理方法,比如Unity中的物理引擎PhysX就是使用引用计数来管理各种对象的。 最后一种方法是本文的重点,即追踪式GC器(Tracing Garbage Collector),Unity使用的GC器是一种叫标记/清除(Mark/Sweep)的算法,它的思路是当程序需要进行垃圾回收时,从根(GC Root)出发标记所有可达对象,然后回收没有标记的对象,这是一种全自动的内存管理方法,程序员完全不用追踪对象的使用情况,也不存在循环引用无法回收的问题,而在Unity中,使用的是一种叫Boehm-Demers-Weiser的GC器,它有以下特点: Stop The World:即当发生GC时,程序的所有线程都必须停止工作,等GC完成才能继续,Unity不支持多线程GC,即使是Unity 2019后使用的增量式GC,在回收时也是要停掉所有线程。 不分代:.NET和Java会把托管堆分成多个代(Generation),新生代的内存空间非常小,而且一般来说,GC主要会集中在新生代上,这让每一次GC的速度也非常快,但是Unity的GC是完全不分代的,即只要发生GC,就会对整个托管堆进行GC(Full GC)。 不压缩:不会对堆内存进行碎片整理,如下图: 图片来源于Unity的官方示例图

不分代:

不压缩:

GC会造成托管堆出现很多这样的空白“间隙”,这些间隙不会合并,当申请一个新对象时,如果没有任何一个间隙大于这个新对象大小,堆内存就会增加。

1.2 为什么要优化GC

优化GC是指降低内存开销,而不是指垃圾回收的过程。

1.GC的触发会导致进程锁死,GC的内存越多,锁死的时间越多.

2.进程自动触发GC,具有不可控性.

1.3 影响GC性能的主要因素

影响GC速度的因素主要有两个: 可达对象数量 托管堆的大小 可达对象是指不会当次GC被回收的对象,减少此类开销的主要方法就是减少对象数量,参考以下实现方法:

class Item
{
      public int a;
      public short b;
}
Item[] items;

对于items数组,每一个元素都会产生一个对象。 而以下代码,不管a和b有多少个元素,数组都只有一个对象,这样就会减少对象数量。

class Item
{
      public int[] a;
      public short[] b;
}
Item item;

而优化托管堆大小主要通过以下几个方面: 减少临时分配:临时分配的内存会产生碎片。 减少内存泄漏:即再也用不到但是又因为存在对其引用无法回收的对象。

1.4 什么时候会触发GC

1.第一代(第0代?)内存不够的时候

2.系统内存不足

3.主动调用GC.Collect()(不是必定会触发)

1.5 容易产生GC的地方以及解决办法

1. New obj

通过复用内存解决:类型池,对象池,容器池。

 2. Boxing(装箱)

如:Dictionary使用枚举或者自定义结构体当做Key

 3. String操作

字符串操作带来的GC很难避免,应尽量避免使用gameObject.name和gameObject.tag。常用字符串使用全局字符串常量。高频率拼接显示字符串的界面可以考虑用ZString,比如大量倒计时界面。

4.匿名函数和闭包

第一次匿名函数会有124B的GC,引用了外部变量每次都会GC,但是并不好避免经常会在回调里面写匿名函数。

 5.委托滥用

同一个委托+=操作触发GC,需要将原来的内存拼接上新的内存,整块移动到新的内存块,GC成指数型增长。

 执行第一次:

 执行第二次:

6. 一些语法糖

2.类和结构

类和结构的区别以及装箱和拆箱,基本上都是老生常谈了,不过,在开发过程中,还是会产生一个疑问:我的数据该使用类还是结构?这个问题接下来的几个部分都会有涉及到。

2.1 如何估算对象和结构体的大小

结构是值类型,它的结构体实例是存放在栈或者堆中的。在栈中我们保有的是实例的值,所以每一次赋值,都会在栈中多赋值一份实例出来。结构体在内存中所占大小,就是其字段所占大小,但是,结构体的大小并不是简单的所有字段的大小相加,而是存在一个对齐规则,在默认的对齐规则中,基本类型字段是按照自身大小对齐的,如byte是按1字节对齐,int是按4字节对齐。如下面的结构体:

  struct S
  {
      byte b1;
  }

这个结构体的大小是1,如果在下面添加一个字段:

  struct S
  {
      byte b1;
      int i1;
  }

这个结构体的大小是8,因为int是4字节对齐的,所以只能从第四个字节开始。 如果再添加一个字段:

  struct S
  {
      byte b1;
      int i1;
      byte b2;
  }

这个结构体的大小是12,由于struct本身也是要对齐的,所以它的对齐规则是按照其中元素最大的对齐规则决定的。如当前这个结构体是按照i1的对齐规则决定的,也就是四字节对齐,不足四字节则不齐。如果想优化其大小,调整顺序如下,结构体的大小就变成了8。

  struct S
  {
      byte b1;
      byte b2;
      int i1;
  }

类是引用类型,它的对象实例存放在堆中,对象实例一定是会占用堆内存的,而在栈中,我们保有的是实例的引用,对象在堆内存中大概是如下图所示: 其中vtable是类的共有数据,包含静态变量和方法表(在Mono中,结构的静态变量也存放在vtable里,它是缓存在一个叫tablecache的哈希表当中的,而IL2CPP中类和结构的静态变量存在一个单独的类里)。Monitor是线程同步用的,这两个指针分别占用一个IntPtr.Size大小(32位中是4字节,64位中是8字节),再下面是所有字段,字段是从第9个字节或17个字节开始的,字段的对齐规则与结构体的对齐规则相同,区别是Mono中对象实例会把引用类型的引用摆在最前面。一个对象实例的大小(instance_size)就是IntPtr.Size * 2+字段所占大小,结构体被装箱后在堆内存的大小也一样。 通过调整字段顺序,可以优化对象和结构体大小,特别是有容器存放多个对象或结构体的,可以减少堆内存占用。 此外,我们还可以通过StructLayoutAttribute自定义类和结构字段的对齐方式。比如下面的结构体:

  [StructLayout(LayoutKind.Sequential, Pack = 1)]
  public struct S
  {
      byte b1;
      int i1;
      byte b2;
  }

该结构体强制按1字节对齐,所以它的大小就是6。

  [StructLayout(LayoutKind.Explicit)]
  public struct S
  {
      [FieldOffset(0)]byte b1;
      [FieldOffset(0)]int i1;
      [FieldOffset(1)] byte b2;
  }

这个结构体的大小是4,它实现了类似C/C++中union的类型,b1、b2与i1共用同一段内存,b1和b2也代表了i1的前两个字节。 注意,内存对齐是会考虑硬件优化的,使用StructLayout修改对齐方式有可能会降低性能。

2.2 装箱和拆箱

装箱和拆箱的过程很多文档都会有描述,这里就不再细说了。只说几个比较

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/jmu201521121014/article/details/107858015

智能推荐

使用nginx解决浏览器跨域问题_nginx不停的xhr-程序员宅基地

文章浏览阅读1k次。通过使用ajax方法跨域请求是浏览器所不允许的,浏览器出于安全考虑是禁止的。警告信息如下:不过jQuery对跨域问题也有解决方案,使用jsonp的方式解决,方法如下:$.ajax({ async:false, url: 'http://www.mysite.com/demo.do', // 跨域URL ty..._nginx不停的xhr

在 Oracle 中配置 extproc 以访问 ST_Geometry-程序员宅基地

文章浏览阅读2k次。关于在 Oracle 中配置 extproc 以访问 ST_Geometry,也就是我们所说的 使用空间SQL 的方法,官方文档链接如下。http://desktop.arcgis.com/zh-cn/arcmap/latest/manage-data/gdbs-in-oracle/configure-oracle-extproc.htm其实简单总结一下,主要就分为以下几个步骤。..._extproc

Linux C++ gbk转为utf-8_linux c++ gbk->utf8-程序员宅基地

文章浏览阅读1.5w次。linux下没有上面的两个函数,需要使用函数 mbstowcs和wcstombsmbstowcs将多字节编码转换为宽字节编码wcstombs将宽字节编码转换为多字节编码这两个函数,转换过程中受到系统编码类型的影响,需要通过设置来设定转换前和转换后的编码类型。通过函数setlocale进行系统编码的设置。linux下输入命名locale -a查看系统支持的编码_linux c++ gbk->utf8

IMP-00009: 导出文件异常结束-程序员宅基地

文章浏览阅读750次。今天准备从生产库向测试库进行数据导入,结果在imp导入的时候遇到“ IMP-00009:导出文件异常结束” 错误,google一下,发现可能有如下原因导致imp的数据太大,没有写buffer和commit两个数据库字符集不同从低版本exp的dmp文件,向高版本imp导出的dmp文件出错传输dmp文件时,文件损坏解决办法:imp时指定..._imp-00009导出文件异常结束

python程序员需要深入掌握的技能_Python用数据说明程序员需要掌握的技能-程序员宅基地

文章浏览阅读143次。当下是一个大数据的时代,各个行业都离不开数据的支持。因此,网络爬虫就应运而生。网络爬虫当下最为火热的是Python,Python开发爬虫相对简单,而且功能库相当完善,力压众多开发语言。本次教程我们爬取前程无忧的招聘信息来分析Python程序员需要掌握那些编程技术。首先在谷歌浏览器打开前程无忧的首页,按F12打开浏览器的开发者工具。浏览器开发者工具是用于捕捉网站的请求信息,通过分析请求信息可以了解请..._初级python程序员能力要求

Spring @Service生成bean名称的规则(当类的名字是以两个或以上的大写字母开头的话,bean的名字会与类名保持一致)_@service beanname-程序员宅基地

文章浏览阅读7.6k次,点赞2次,收藏6次。@Service标注的bean,类名:ABDemoService查看源码后发现,原来是经过一个特殊处理:当类的名字是以两个或以上的大写字母开头的话,bean的名字会与类名保持一致public class AnnotationBeanNameGenerator implements BeanNameGenerator { private static final String C..._@service beanname

随便推点

二叉树的各种创建方法_二叉树的建立-程序员宅基地

文章浏览阅读6.9w次,点赞73次,收藏463次。1.前序创建#include<stdio.h>#include<string.h>#include<stdlib.h>#include<malloc.h>#include<iostream>#include<stack>#include<queue>using namespace std;typed_二叉树的建立

解决asp.net导出excel时中文文件名乱码_asp.net utf8 导出中文字符乱码-程序员宅基地

文章浏览阅读7.1k次。在Asp.net上使用Excel导出功能,如果文件名出现中文,便会以乱码视之。 解决方法: fileName = HttpUtility.UrlEncode(fileName, System.Text.Encoding.UTF8);_asp.net utf8 导出中文字符乱码

笔记-编译原理-实验一-词法分析器设计_对pl/0作以下修改扩充。增加单词-程序员宅基地

文章浏览阅读2.1k次,点赞4次,收藏23次。第一次实验 词法分析实验报告设计思想词法分析的主要任务是根据文法的词汇表以及对应约定的编码进行一定的识别,找出文件中所有的合法的单词,并给出一定的信息作为最后的结果,用于后续语法分析程序的使用;本实验针对 PL/0 语言 的文法、词汇表编写一个词法分析程序,对于每个单词根据词汇表输出: (单词种类, 单词的值) 二元对。词汇表:种别编码单词符号助记符0beginb..._对pl/0作以下修改扩充。增加单词

android adb shell 权限,android adb shell权限被拒绝-程序员宅基地

文章浏览阅读773次。我在使用adb.exe时遇到了麻烦.我想使用与bash相同的adb.exe shell提示符,所以我决定更改默认的bash二进制文件(当然二进制文件是交叉编译的,一切都很完美)更改bash二进制文件遵循以下顺序> adb remount> adb push bash / system / bin /> adb shell> cd / system / bin> chm..._adb shell mv 权限

投影仪-相机标定_相机-投影仪标定-程序员宅基地

文章浏览阅读6.8k次,点赞12次,收藏125次。1. 单目相机标定引言相机标定已经研究多年,标定的算法可以分为基于摄影测量的标定和自标定。其中,应用最为广泛的还是张正友标定法。这是一种简单灵活、高鲁棒性、低成本的相机标定算法。仅需要一台相机和一块平面标定板构建相机标定系统,在标定过程中,相机拍摄多个角度下(至少两个角度,推荐10~20个角度)的标定板图像(相机和标定板都可以移动),即可对相机的内外参数进行标定。下面介绍张氏标定法(以下也这么称呼)的原理。原理相机模型和单应矩阵相机标定,就是对相机的内外参数进行计算的过程,从而得到物体到图像的投影_相机-投影仪标定

Wayland架构、渲染、硬件支持-程序员宅基地

文章浏览阅读2.2k次。文章目录Wayland 架构Wayland 渲染Wayland的 硬件支持简 述: 翻译一篇关于和 wayland 有关的技术文章, 其英文标题为Wayland Architecture .Wayland 架构若是想要更好的理解 Wayland 架构及其与 X (X11 or X Window System) 结构;一种很好的方法是将事件从输入设备就开始跟踪, 查看期间所有的屏幕上出现的变化。这就是我们现在对 X 的理解。 内核是从一个输入设备中获取一个事件,并通过 evdev 输入_wayland

推荐文章

热门文章

相关标签