如何优化结构体和类的大小?

类和结构体的区别,大家应该都知道,但是在开发过程中到底是用类还是结构呢?

要想知道这个问题的答案首先应该知道怎样估算对象和结构体的大小。

一、如何估算结构体的大小

结构是值类型,它的结构体的实例是存放在栈中或者堆中。结构体在内存中所占的大小,就是其字段所占的大小,但是,它的大小并不是所有字段大小相加,而是存在一个对齐的规则,在默认的对齐规则中,基本类型字段是按照自身大小对齐的,如byte是按1字节对齐。

struct A
{
byte a1;
}

如上面这个结构体的大小就是1字节,如果是下面这个:

struct A
{
byte a1;
int a2;
}

这个结构体所占内存大小是8字节,因为int是4字节对齐的,所以只能从第四个字节开始。

如果再添加一个字段:

struct A
{
byte a1;
int a2;
byte a3;
}

这个结构体大小是12,由于struct本身也要是对齐的,所以它的对齐规则是按照其中元素最大的对齐规则决定的。也就是说上面这个结构体要按照4字节对齐,不足4字节要补齐,所以是12个字节大小。

如果想要优化它的大小,可以调整顺序如下:

struct A
{
byte a1;
byte a3;
int a2;
}

这个时候这个结构体所占的大小就是8字节了。

二、如何估算类的大小

类是引用类型,它的对象实例存放在堆中,对象实例一定是会占用堆内存的,而在栈中,保存的是实例的引用。对象在堆中分成3个区域,vtable、monitor和字段。其中vtable是类的共有数据,包含静态变量和方法表,这个应该就是类本身所占用的大小和具体的对象无关。monitor是线程同步用的,这2个指针分别占用一个inptr.Size大小,字段是从第9个字节或17个字节开始的,字段的对齐规则和结构体的对齐规则相同,区别是Mono中对象的实例会把引用类型的引用放在最前面。一个对象实例的大小就是 inptr.Size *2+字段的大小。

通过调整字段的顺序,也可以优化对象的大小。

还可以通过StructLayoutAttribute自定义类和结构体的对齐方式。

[StructLayout(LayoutKind, Sequential, Pack = 1)]
public struct A
{
byte a1;
int a2;
byte a3;
}

上面这个结构体强制按照1字节对齐,所以他的大小是6字节。但是这样做可能会降低性能。

所以具体情况还是要具体分析,懂得了如何估算结构体和类的大小,就更容易知道该如何使用它们了。

unity3D优化之内存管理

在U3D游戏中内存管理一直都是让人比较头疼事情,现在手机游戏是越做越大,和端游一样,每次卡顿和每次的内存增长对玩家来说都是一个比较差的体验。听到过一句话,说游戏开发做久了就会变成“GC怪”,因为在游戏开发过程中,需求变化多,功能不停的迭代,内存问题也一直存在,需要不停的去优化它。

在unity2018中集成来正版的.NET4.X和C#7.3,引入了ref return 和ref locals,让值类型的操作更加便捷,在U3D2019中更是加入来增量式GC,减少来GC带来的卡顿问题。比以前的版本灵活来许多。

讲了这么多,可能很多人还不知道什么是GC,GC的全称是Garbage Collection,也就是垃圾回收的意思,是一种自动管理堆内存的机制,管理堆内存上对象的分配和释放。

一、内存管理方式

我们常用的内存管理方式有三种:

1.手动管理,像C/C++一样使用malloc/free或者new/delete来为对象分配释放内存。这张方法的优点是速度快,没有任何额外开销,缺点是要去人工了解每个对象的使用情况,这样很容易发生各种问题,比如内存泄漏,野指针和空悬指针等。

2.使用引用计数,它的思想是对象创建出来以后,维护一个针对该对象的计数,使用该对象的地方对该计数加1,使用完后减1,当计数为0时,销魂该对象。这种方法类似半自动内存管理方式,优点是可以把分配和释放的开销分布在实际使用过程当中,速度比较快,不过会存在一个循环引用的问题。引用计数是一种比较常用的内存管理方法,比如U3D中物理引擎PhysX就是使用引用计数来管理各种对象的。

3.追踪式GC器,unity使用的GC器是一种叫标记/清除的算法,它的思路是当程序需要进行垃圾回收时,从根出发标记所有可以到达的对象,然后回收没有标记的对象,这是一种全自动的内存管理方法,程序员完全不用追踪对象的使用情况,也不存在循环引用无法回收的问题。在unity中使用的是一种叫boehm-Demers-Weiser的GC器,它的特点是:

(1)stop the world ,即当GC发生时,程序的所有线程都必须停止工作,在回收时也要停掉所有线程。

(2)不分代,.NET和java会把托管堆分成多个代,新生代的内存空间非常小,而且一般来说,GC主要集中在新生代上,让每一次GC的速度很快,但是在U3D中GC是完全不分代的,只要发生GC,就会对整个托管堆进行GC。

(3)不压缩,不会对堆内存进行碎片整理,类似我们的磁盘一样,使用久了就会有很多的碎片,造成磁盘上有很多小空隙。同样在U3D中GC会造成托管堆出现很多这样的间隙,这些间隙不会合并,当申请一个新对象时,如果没有任何一个间隙大于这个新对象的大小,堆内存就会增加。

二、影响GC性能的因素

主要因素有2个:

1、可达对象的数量

2、托管堆的大小

可达对象是不会被GC回收的对象,减少这类对象的方法是减少对象的数量,如下:

将会产生10个Actor对象。

如果使用以下方法:

只产生一个对象,方法很简单,但是却很有用,如果在需要每帧去处理的对象上去使用这类优化,会得到意想不到的效果。

而优化托管堆的大小主要通过以下几个方面:

1、减少临时内存分配,因为临时内存会使内存短暂的增长,而且会产生碎片

2、防止内存泄漏,也就是存在互相引用无法回收对象的情况,或者没用到的对象,但是对它还有引用,导致释放不掉。