如何处理返回结构的不变性?

Ste*_*nov 8 .net c# language-features struct immutability

我正在写一个拥有巨大2D"细胞"阵列的游戏.一个单元只需3个字节.我还有一个名为CellMap的类,它包含2D数组作为私有字段,并通过公共索引器提供对它的访问.

Profiling showed that a performance problem is caused by garbage collection of too many Cell objects. So I decided to make Cell a struct (it was a class).

But now code like this doesn't work:

cellMap[x, y].Population++;
Run Code Online (Sandbox Code Playgroud)

I can think of many options, but I don't really like any of them.

  1. Make the array public, and write cellMap.Data[x, y].Population = 5;
  2. Stop using a CellMap class, and just use a 2D array directly. But CellMap is very convenient because it implements its own optimized serialization, and it exposes Width and Height properties that are more convenient than writing cellMap.GetLength(0)
  3. Make Cell immutable. But then how would the code look? cellMap[x, y] = IncrementCellPopulation(cellMap[x, y])? Very verbose.
  4. A couple of utility functions like cellMap.SetPopulationAt(x, y, 5)
  5. 在每一个拥有CellMap类,添加一个实用性质一样private Cell[,] CellData { get { return this.CellMap.GetInternalArray(); } },所以后来我的代码可以看起来像CellData[x, y].Population++

这个问题传统上是如何解决的?

Eri*_*ert 18

所以这里实际上有两个问题.你实际问的问题是:什么是处理结构应该是不可变的这一事实的技术,因为它们是按值复制的,但你想要改变一个.然后有一个问题是激励这个问题,"我怎样才能使我的程序的性能可以接受?"

我的另一个答案解决了第一个问题,但第二个问题也很有趣.

首先,如果探查器实际上已经确定性能问题是由于单元格的垃圾收集,则可能将单元格转换为结构将有所帮助.也有可能它根本没有帮助,这样做可能会使情况变得更糟.

您的单元格不包含任何引用类型; 我们知道这是因为你说他们只有三个字节.如果读到这个的人认为他们可以通过将类转换为结构来进行性能优化,那么它可能根本没有帮助,因为类可能包含引用类型的字段,在这种情况下,垃圾收集器仍然必须收集每个实例,即使它被转换为值类型.其中的引用类型也需要收集!如果Cell仅包含值类型,我建议仅出于性能原因尝试此操作,显然它确实如此.

它可能会使情况变得更糟,因为价值类型不是灵丹妙药; 他们也有成本.复制值的类型通常比引用类型更昂贵(它几乎总是寄存器的大小,几乎总是在适当的存储器边界上对齐,因此芯片高度优化以便复制它们).并且值类型始终被复制.

现在,在您的情况下,您有一个小于参考的结构; 引用通常是四个或八个字节.你把它们放在一个数组中,这意味着你正在将数组打包下来; 如果你有一千个,它将需要三千字节.这意味着其中每四个结构中有三个未对齐,这意味着有更多时间(在许多芯片架构上)从阵列中获取值.您可以考虑将结构填充的影响测量到四个字节以查看是否会产生影响,前提是您仍然要将它们保存在一个数组中,这将我带到下一个点......

Cell存储抽象可能只是一个糟糕的抽象,用于存储大量单元格的数据.如果问题是单元格是类,那么你要保留数千个单元格的数组,并且收集它们是昂贵的,那么除了将Cell变成结构之外,还有其他解决方案.例如,假设一个Cell包含两个字节的Population和一个字节的Color.这是Cell 的机制,但肯定不是你想要向用户公开的接口.您的机制没有理由必须使用与接口相同的类型.因此,您可以按需制作Cell类的实例:

interface ICell
{
   public int Population { get; set; }
   public Color Color { get; set; }
}
private class CellMap
{
    private ushort[,] populationData; // Profile the memory burden vs speed cost of ushort vs int
    private byte[,] colorData; // Same here. 
    public ICell this[int x, int y] 
    {
        get { return new Cell(this, x, y); }
    }

    private sealed class Cell : ICell
    {
        private CellMap map;
        private int x;
        private int y;
        public Cell(CellMap map, int x, int y)
        {
            this.map = map; // etc
        }
        public int Population  
        {
            get { return this.map.populationData[this.x, this.y]; } 
            set { this.map.populationData[this.x, this.y] = (ushort) value; } 
        }
Run Code Online (Sandbox Code Playgroud)

等等.按需制造电池.如果它们是短暂的,它们几乎会立即被收集.CellMap是一个抽象,所以使用抽象来隐藏凌乱的实现细节.

使用这种架构,您没有任何垃圾收集问题,因为您几乎没有活动的Cell实例,但您仍然可以说

map[x,y].Population++;
Run Code Online (Sandbox Code Playgroud)

没问题,因为第一个索引器制造了一个知道如何更新地图状态的不可变对象.该细胞并不需要是可变的; 注意Cell类是完全不可变的.(哎呀,Cell在这里可能是一个结构,但当然把它投射到ICell只会装箱.)它是可变的地图,并且单元格为用户改变了地图.


Eri*_*ert 9

如果你想让Cell不可变 - 就像它应该是一个结构 - 那么一个好的技术就是在Cell上创建一个实例方法的工厂:

struct C
{
    public int Foo { get; private set; }
    public int Bar { get; private set; }
    private C (int foo, int bar) : this()
    {
        this.Foo = foo;
        this.Bar = bar;
    }
    public static C Empty = default(C);
    public C WithFoo(int foo)
    {
        return new C(foo, this.Bar);
    }
    public C WithBar(int bar)
    {
        return new C(this.Foo, bar);
    }
    public C IncrementFoo()
    {
        return new C(this.Foo + 1, bar);
    }
    // etc
}
...
C c = C.Empty;
c = c.WithFoo(10);
c = c.WithBar(20);
c = c.IncrementFoo();
// c is now 11, 20
Run Code Online (Sandbox Code Playgroud)

所以你的代码会是这样的

map[x,y] = map[x,y].IncrementPopulation();
Run Code Online (Sandbox Code Playgroud)

但是,我认为这可能是一条死胡同; 最好不要在一开始就没有那么多Cell,而不是试图优化一个有数千个Cell的世界.我会写另一个答案.


lep*_*pie 4

6. ref在改变值的方法中使用参数,将其称为IncrementCellPopulation(ref cellMap[x, y])

  • @Steven:“ref”适用于*变量*,因为*ref为变量*创建了别名。变量可以*变化*,这就是它们被称为*变量*的原因,因此要更改不可变结构,您必须更改变量中的值。(整数是不可变的,但您可以更改变量中的整数。)所以现在的问题是“什么是变量?” 局部变量、字段、数组元素和指针取消引用都是变量。属性和索引器不是;这些实际上是对 getter 和 setter 的方法调用,而不是变量访问。 (5认同)