fixed in CSharp
1、固定托管对象的地址
.NET的GC在进行可达性遍历后会将所有使用到的内存进行压缩(Compact),空出一整块未使用的内存方便后续申请使用,这时候,原先代码里还在使用的内存就可能会变动位置,指针就会失效。所以fixed在这里的作用就是告诉GC不要compact我这个托管对象,常常配合指针进行较为底层的操作。
1 | unsafe |
- 这种方式会有一定内存损耗,可能会影响GC产生内存碎片。
- fixed只用用于内建类型数组,别用自己引用类型元素数组,它是不会递归pin的
2、固定大小缓冲区字段fixed buffer
在C#中,在结构体里int[] Arr声明的数组,实际在内存中的结构体里存的是一个引用,这个引用指向堆内存中的数组,需要进行二次访问。这样很合理,因为结构体需要事先确定好大小,但是数组又可以运行时动态构造大小,所以只能存一个固定大小的指针,然后实际内容你自己去堆里申请吧。
对于性能要求较高的领域,比如ECS系统中,本身就是为了极致利用硬件缓存命中设计的,ECS系统里会有大量组件,存储在连续的内存里。如果一个组件需要包含数组,这时候你用普通的数组,CPU在Query组件并批量访问的时候,就需要在期间跳跃到堆内存的各个地方,性能就会大打折扣。这时候就轮到fixed闪亮登场了。
这里的用法就让我想起C语言了,在声明时严格要求编译期确认数组大小,这样数组固定下来,自然可以存放在一起。这时数组就和普通的内建值类型一样。
- 一般包含固定缓冲区的结构体都比较大,建议传参时用in和ref传引用而非副本。
- 这个固定缓冲区写法和数组是有区别的,数组是
int[] arr => type varName,数组的类型就是int[],而这里是fixed int fixedBuffer[3] => fixed type varName[length]
1 | // 数组会嵌入结构体那段连续内存里[A,arr[0],arr[1],arr[2],arr[3],B] |
2.1 实际应用
在ECS项目里,经常遇到各种表示状态、标志位、Debuff的临时组件被添加到Entity身上,在基于Archetype-Chunk的ECS体系里,意味着实体以及其组件数据经常需要被移动(内存抖动)。
简单介绍下ECS相关背景:Archetype表示的是特定类型组件的集合,内部的chunk会连续存储包含的所有组件。举例,一个Archetype包含2个组件,CompA占用20B,CompB占用12B,总体占用32B。16KB/32B=512,也就是一个Chunk最多可以存储512个包含CompA和CompB的实体(假设实体本身不占用chunk)。最后,chunk里的内存类似[[CompA1][CompA2]...[CompA512][CompB1][CompB2]...[CompB512]]这种SoA形式
说这么多,只想明确一件事:一个chunk在被某个Archetype创建出来后就确定了内部的内存布局,它不会重新划分布局去容纳新加入或删除的组件,如果发生了组件的添加和移除,需要到另一个Archetype下重新创建或者找到位置将数据拷贝过去。因此在用ECS的时候我们总希望所有组件都是事先确定的。
为了缓解内存抖动,我们希望引入一个复合组件来维护这类状态标志位,并包含计数功能,这样即使频发添加移除,也不会触发内存的搬移。
下面的例子有几个关注点:
- 通过fixed的固定缓冲区字段充当标志位(Flags枚举)的计数器
- 维护_mask来快速判断有无
- 用哈希映射替换Log2(x)或者循环移位计算Log2(x)
- 缺点就是,无法通过独立组件灵活进行ECS的Query,比如想查询带有Debuff1的实体,原先只需要WithAll
()就可以直接找到,现在需要WithAll 然后再检回的结果内再判断状态是否包含Debuff1的标志位
1 | // 作为一个ECS组件 |