How to deal with the immutability of returned structs?

前端 未结 6 1854

I\'m writing a game that has a huge 2D array of \"cells\". A cell takes only 3 bytes. I also have a class called CellMap, which contains the 2D array as a private field, and pro

6条回答
  •  有刺的猬
    2021-02-04 17:20

    So there are actually two problems here. There's the question you actually asked: what are techniques to deal with the fact that structs ought to be immutable because they are copied by value, but you want to mutate one. And then there's the question which is motivating this one, which is "how can I make the performance of my program acceptable?"

    My other answer addresses the first question, but the second question is interesting as well.

    First off, if the profiler has actually identified that the performance problem is due to garbage collection of cells, then it is possible that making cell into a struct will help. It is also possible that it will not help at all, and it is possible that doing so will make it worse.

    Your cells do not contain any reference types; we know this because you've said they are only three bytes. If someone else reading this is thinking that they could make a performance optimization by turning a class into a struct then it might not help at all because the class might contain a field of reference type, in which case the garbage collector still has to collect every instance, even if it is turned into a value type. The reference types in it need to be collected too! I would only recommend attempting this for performance reasons if Cell contains only value types, which apparently it does.

    It might make it worse because value types are not a panacea; they have costs too. Value types are often more expensive to copy than reference types (which are pretty much always the size of a register, almost always aligned on the appropriate memory boundary, and therefore the chip is highly optimized for copying them). And value types are copied all the time.

    Now, in your case you have a struct which is smaller than a reference; references are four or eight bytes typically. And you're putting them in an array, which means that you are packing the array down; if you have a thousand of them, it'll take three thousand bytes. Which means that three out of every four structs in there are misaligned, meaning more time (on many chip architectures) to get the value out of the array. You might consider measuring the impact of padding your struct out to four bytes to see if that makes a difference, provided you're still going to keep them in an array, which brings me to my next point...

    The Cell abstraction might simply be a bad abstraction for the purpose of storing data about lots of cells. If the problem is that Cells are classes, you're keeping an array of thousands of Cells, and collecting them is expensive, then there are solutions other than making Cell into a struct. Suppose for example that a Cell contains two bytes of Population and one byte of Color. That is the mechanism of Cell, but surely that is not the interface you want to expose to the users. There is no reason why your mechanism has to use the same type as the interface. And therefore you could manufacture instances of the Cell class on demand:

    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; } 
            }
    

    and so on. Manufacture the cells on demand. They will almost immediately be collected if they are short-lived. CellMap is an abstraction, so use the abstraction to hide the messy implementation details.

    With this architecture you don't have any garbage collection problems because you have almost no live Cell instances, but you can still say

    map[x,y].Population++;
    

    no problem, because the first indexer manufactures an immutable object which knows how to update the state of the map. The Cell doesn't need to be mutable; notice that the Cell class is completely immutable. (Heck, the Cell could be a struct here, though of course casting it to ICell would just box it anyway.) It is the map which is mutable, and the cell mutates the map for the user.

提交回复
热议问题