【问题标题】:C# Memory ConcernsC# 内存问题
【发布时间】:2020-02-19 06:47:05
【问题描述】:

我与cellular automata 合作。我的工作回购是here。基本结构是

1) 一个网格 2) 细胞,可能有 3) 代理。

代理根据一组规则行事,通常一个人为代理指定“状态”(不同状态的代理有不同的规则)。一个(相对)知名的 CA 是 game of life

我正在尝试扩展一些东西,并在我的 CA 中加入其他类型的“属性”,主要是为了模拟各种现象(想象一个消耗植物代理的动物代理,以及植物的“生物质”或具有你减少了)。

为此,我合并了一个普通字典,其中字符串作为键,一个名为 CAProperty 的结构作为值。结构如下:

public struct CAProperty
{
    public readonly string name;
    public readonly dynamic value;
    //public readonly Type type;
    public CAProperty(string name, dynamic value)
    {
        this.name = name;
        this.value = value;
    }
}

(注意:以前我有一个“类型”变量来在运行时启用准确的输入......但为了解决这篇文章中的问题,我删除了它。事实上,它需要重新添加)

这很好。但是,我正在尝试使用大网格尺寸做一些工作。 100x100。 1000x1000。 5000x5000,或 2500 万个细胞(和代理)。那将是 2500 万个字典。

查看图片:来自 Visual Studio 的内存快照,用于 4000x4000 网格或 1600 万个代理(我尝试了 5000x5000,但 Visual Studio 不允许我拍摄快照)。

在右侧,可以清楚地看到调试器正在读取 8 GB 的内存使用情况(我在发布版本中尝试过此操作以查看 6875 MB 的使用情况)。但是,当我计算快照第三列中的所有内容时,我发现不到 4 GB。

为什么总内存使用量与存储在内存中的对象大小之间存在如此巨大的差异?

另外:我如何优化内存使用(主要是字典 - 是否有另一个具有类似行为但内存使用量较低的集合)?

编辑:对于三个“组件”(网格、单元格、代理)中的每一个,我都有一个类。它们都继承自原始的 CAEntity 类。全部如下所示。

    public abstract class CAEntity
    {
        public CAEntityType Type { get; }
        public Dictionary<string, dynamic> Properties { get; private set; }

        public CAEntity(CAEntityType type)
        {
            this.Type = type;
        }

        public CAEntity(CAEntityType type, Dictionary<string, dynamic> properties)
        {
            this.Type = type;
            if(properties != null)
            {
                this.Properties = new Dictionary<string, dynamic>(properties);
            }
        }
    }

    public class CAGraph:CAEntity
    {
        public ValueTuple<ushort, ushort, ushort> Dimensions { get; }
        public CAGraphCell[,,] Cells { get;}
        List<ValueTuple<ushort, ushort, ushort>> AgentCells { get; set; }
        List<ValueTuple<ushort, ushort, ushort>> Updates { get; set; }
        public CA Parent { get; private set; }
        public GridShape Shape { get; }
        //List<double> times = new List<double>();
        //System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();

        public CAGraph (CA parent, ValueTuple<ushort, ushort, ushort> size, GridShape shape):base(CAEntityType.Graph)
        {
            this.Parent = parent;
            this.Shape = shape;
            AgentCells = new List<ValueTuple<ushort, ushort, ushort>>();
            Updates = new List<ValueTuple<ushort, ushort, ushort>>();
            Dimensions = new ValueTuple<ushort, ushort, ushort>(size.Item1, size.Item2, size.Item3);
            Cells = new CAGraphCell[size.Item1, size.Item2, size.Item3];
            for (ushort i = 0; i < Cells.GetLength(0); i++)
            {
                for (ushort j = 0; j < Cells.GetLength(1); j++)
                {
                    for (ushort k = 0; k < Cells.GetLength(2); k++)
                    {
                        Cells[i, j, k] = new CAGraphCell(this, new ValueTuple<ushort, ushort, ushort>(i, j, k));
                    }
                }
            }
        }

        public CAGraph(CA parent, ValueTuple<ushort, ushort, ushort> size, GridShape shape, List<ValueTuple<ushort, ushort, ushort>> agents, CAGraphCell[,,] cells, Dictionary<string, dynamic> properties) : base(CAEntityType.Graph, properties)
        {
            Parent = parent;
            Shape = shape;
            AgentCells = agents.ConvertAll(x => new ValueTuple<ushort, ushort, ushort>(x.Item1, x.Item2, x.Item3));
            Updates = new List<ValueTuple<ushort, ushort, ushort>>();
            Dimensions = new ValueTuple<ushort, ushort, ushort>(size.Item1, size.Item2, size.Item3);
            Cells = new CAGraphCell[size.Item1, size.Item2, size.Item3];
            for (ushort i = 0; i < size.Item1; i++)
            {
                for (ushort j = 0; j < size.Item2; j++)
                {
                    for (ushort k = 0; k < size.Item3; k++)
                    {
                        //if(i == 500 && j == 500)
                        //{
                        //    Console.WriteLine();
                        //}
                        Cells[i, j, k] = cells[i, j, k].Copy(this);
                    }
                }
            }
        }
    }

    public class CAGraphCell:CAEntity
    {
        public CAGraph Parent { get; set; }
        public CAGraphCellAgent Agent { get; private set; }
        public ValueTuple<ushort, ushort, ushort> Position { get; private set; }
        //private Tuple<ushort, ushort, ushort>[][] Neighbors { get; set; }
        //System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();

        public CAGraphCell(CAGraph parent, ValueTuple<ushort, ushort, ushort> position):base(CAEntityType.Cell)
        {
            this.Parent = parent;
            this.Position = position;
            //this.Neighbors = new Tuple<ushort, ushort, ushort>[Enum.GetNames(typeof(CANeighborhoodType)).Count()][];
        }

        public CAGraphCell(CAGraph parent, ValueTuple<ushort, ushort, ushort> position, Dictionary<string, dynamic> properties, CAGraphCellAgent agent) :base(CAEntityType.Cell, properties)
        {
            this.Parent = parent;
            this.Position = position;
            if(agent != null)
            {
                this.Agent = agent.Copy(this);
            }
        }
    }

    public class CAGraphCellAgent:CAEntity
    {
        // have to change...this has to be a property? Or no, it's a CAEntity which has a list of CAProperties.
        //public int State { get; set; }
        public CAGraphCell Parent { get; set; }
        //System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();

        public CAGraphCellAgent(CAGraphCell parent, ushort state):base(CAEntityType.Agent)
        {
            this.Parent = parent;
            AddProperty(("state", state));
        }

        public CAGraphCellAgent(CAGraphCell parent, Dictionary<string, dynamic> properties) :base(CAEntityType.Agent, properties)
        {
            this.Parent = parent;
        }
    }

【问题讨论】:

  • 那么字典键和 CAProperty 名称之间有区别吗?如果是,为什么?如果没有,为什么不是Dictionary&lt;string,dynamic&gt;
  • ...不,没有区别。最初我计划包含 Type 变量,但也许这不是绝对必要的。我打算让这个结构支持数字类型和布尔值,但也许我可以将它限制为数字类型(动态可以可靠地键入它们)。我试一试,切换到 Dictionary。谢谢。
  • 我尝试消除 CAProperty 结构并只使用 Dictionary,但它似乎使用了大约相同数量的内存。

标签: c# dictionary memory simulation cellular-automata


【解决方案1】:

听起来您的问题是您的代理表示(使用字典)消耗了太多内存。如果是这样,解决方案是找到更紧凑的表示。

由于您使用的是面向对象的语言,典型的解决方案是定义一个代理类,可能为不同类型的代理定义子类,并使用实例变量来存储每个代理的状态。然后,您的 CA 网格将是一组 Agent 实例(或者可能是空单元格的空值)。这将比使用带有字符串键的字典紧凑得多。

另外,我建议不要将代理在网格上的位置作为代理状态的一部分存储,而是将其作为参数传递给任何需要它的方法。这不仅本身节省了一点内存,而且还允许您在网格上的多个单元格中放置对同一代理实例的引用,以表示多个相同的代理。根据您的 CA 中出现此类相同代理的频率,这可能会节省大量内存。

请注意,如果您修改此类重用代理实例的状态,则该修改显然会影响该实例所代表的网格上的所有代理。因此,最好让您的代理对象不可变,并在代理的状态发生变化时创建一个新对象。

可能还想考虑维护网格上已经存在的代理实例的缓存(例如一组),以便您可以轻松检查新代理是否与现有代理相同。这是否真的有用取决于您的特定 CA 模型 — 使用某些 CA,即使没有这样的缓存,您也可能能够很好地处理重复数据删除(拥有 一些 重复的 Agent 对象是完全可以的),而对于其他人来说,可能根本没有足够的相同代理使其值得。此外,如果您尝试这样做,请注意您需要将缓存设计为使用弱引用(这可能很难做到正确)或定期清除并重建它以避免旧的代理对象即使在它们之后仍留在缓存中'已从网格中删除。


附录基于您在下面的评论,我将在此处引用:

想象一个温度随季节变化的环境(图上的属性)。有陆地和水细胞(因此细胞的特性),并且在足够低的温度下,水细胞会冻结,因此动物代理可以使用它们在陆地位置之间穿越。想象一下那些动物代理人猎杀其他动物代理人吃它们(所以代理人的属性)。想象一下,被吃掉的动物代理会吃树(因此其他具有属性的代理),并且倾向于吃幼苗(限制树木的生长),从而限制了它们自身的生长(以及食肉动物代理的生长)。

好的,让我们勾勒出您需要的课程。 (请原谅任何语法错误;我不是真正的 C# 程序员,我还没有实际测试过这段代码。只要把它想象成类似 C# 的伪代码之类的。)

首先,您显然需要一堆代理。让我们为它们定义一个抽象超类:

public abstract class Agent {
    public abstract void Act(Grid grid, int x, int y, float time);
}

我们的 CA 模拟(为简单起见,我将假设它是随机的,即代理以随机顺序一次行动一个,就像在 Gillespie algorithm 中一样)基本上会反复涉及在网格上选择一个随机单元格 (x, y),检查该单元格是否包含代理,如果是,则在该代理上调用 Act()。 (在执行此操作时,我们还需要更新任何与时间相关的全局状态,但我们稍后再说。)

代理的Act() 方法将接收对网格对象的引用,并可以调用其方法来更改附近单元格的状态(甚至获取对这些单元格中代理的引用并调用它们的方法直接地)。这可能涉及例如从网格中删除另一个代理(因为它刚刚被吃掉),添加一个新代理(复制),更改代理代理的位置(移动),甚至从网格中删除该代理(例如,因为它饿死或死于老年)。为了便于说明,让我们画出几个代理类:

public class Sapling : Agent {
    private static readonly double MATURATION_TIME = 10;  // arbitrary time delay

    private double birthTime;  // could make this a float to save memory
    public Sapling(double time) => birthTime = time;

    public override void Act(Grid grid, int x, int y, double time) {
        // if the sapling is old enough, it replaces itself with a tree
        if (time >= birthTime + MATURATION_TIME) {
            grid.SetAgentAt(x, y, Tree.INSTANCE);
        }
    }
}

public class Tree : Agent {
    public static readonly Tree INSTANCE = new Tree();

    public override void Act(Grid grid, int x, int y, double time) {
        // trees create saplings in nearby land cells
        (int x2, int y2) = grid.RandomNeighborOf(x, y);
        if (grid.GetAgentAt(x2, y2) == null && grid.CellTypeAt(x2, y2) == CellType.Land) {
            grid.SetAgentAt(x2, y2, new Sapling(time));
        }
    }
}

为了简洁起见,我将把动物代理的实现留作练习。此外,上面的 Tree 和 Sapling 实现有点粗糙,可以通过各种方式进行改进,但它们至少应该说明这个概念。

值得注意的是,为了尽量减少内存使用,上面的代理类具有尽可能少的内部状态。特别是,代理不会将自己的位置存储在网格上,而是将其作为act() 方法的参数接收。因为省略位置实际上使我的 Tree 类完全无状态,所以我继续为网格上的所有树使用 same 全局 Tree 实例!虽然这并不总是可行的,但如果可行,它可以节省大量内存。

现在,网格呢?一个基本的实现(暂时忽略不同的单元格类型)看起来像这样:

public class Grid {
    private readonly int width, height;
    private readonly Agent?[,] agents;

    public Grid(int w, int h) {
        width = w;
        height = h;
        agents = new Agent?[w, h];
    }

    // TODO: handle grid edges
    public Agent? GetAgentAt(int x, int y) => agents[x, y];
    public void SetAgentAt(int x, int y, Agent? agent) => agents[x, y] = agent;
}

现在,细胞类型呢?您有几种方法可以处理这些问题。

一种方法是让网格存储一个 Cell 对象数组而不是代理,并让每个单元存储其状态和(可能)一个代理。但是为了优化内存使用,最好只使用一个单独的 2D 数组来存储单元类型,如下所示:

public enum CellType : byte { Land, Water, Ice }

public class Grid {
    private readonly Random rng = new Random();
    private readonly int width, height;
    private readonly Agent?[,] agents;
    private readonly CellType[,] cells;  // TODO: init in constructor?

    private float temperature = 20;  // global temperature in Celsius

    // ...

    public CellType CellTypeAt(int x, int y) {
        CellType type = cells[x,y];
        if (type == CellType.Water && temperature < 0) return CellType.Ice;
        else return type;
    }
}

注意 CellType 枚举是如何基于字节的,与基于 int 的情况相比,这应该使存储它们的数组更加紧凑。

现在,让我们最后看看主 CA 模拟循环。在最基本的情况下,它可能如下所示:

Grid grid = new Grid(width, height);
grid.SetAgentAt(width / 2, height / 2, Tree.INSTANCE);

// average simulation time per loop iteration, assuming that each
// actor on the grid acts once per unit time step on average
double dt = 1 / (width * height);

for (double t = 0; t < maxTime; t += dt) {
    (int x, int y) = grid.GetRandomLocation();
    Agent? agent = grid.GetAgentAt(x, y);
    if (agent != null) agent.Act(grid, x, y, t);
    // TODO: update temperature here?
}

(从技术上讲,为了正确实现 Gillespie 算法,迭代之间的模拟时间增量应该是一个 exponentially distributed 随机数,平均值为 dt,而不是一个恒定增量。但是,由于只有 @987654335 之一上的参与者在每次迭代中选择@单元,同一演员的动作之间的迭代次数为geometrically distributed,平均值为width * height,并将其乘以dt = 1 / (width * height)给出了指数分布的极好近似值平均值为 1。这是一种冗长的说法,在实践中使用恒定的时间步长是非常好的。)

既然已经够长了,我就让你从这里继续。我只想指出,有很多方法可以进一步扩展和/或优化我上面勾勒的算法。

例如,您可以通过维护包含现场演员的所有网格位置列表并从该列表中随机抽样演员来加快模拟速度(但您还需要按时间步长的倒数缩放时间步长)列表的长度)。此外,您可能会决定希望某些演员比其他演员获得更多的表演机会;虽然做到这一点的简单方法就是使用拒绝采样(即,只有在 rng.Sample() &lt; prob 对一些介于 0 和 1 之间的 prob 时,演员才做某事),但更有效的方法是维护多个位置列表,具体取决于那里的演员类型。

【讨论】:

  • 查看我的编辑:我确实有一个 Agent 类。不幸的是,单个代理不能成为实例,因为每个代理都在进行独立的计算(当然基于特定于状态的规则集),例如 this paper
  • 我不确定为什么您认为“单个代理不能成为实例,因为每个代理都在进行独立计算”。在任何情况下,您的代码(看起来并不完整——您还没有展示您的代理实际做什么)似乎过于复杂。一方面,我很确定您的“CAGraphCell”类完全没用:只需将您的代理直接放在网格上即可。而且我不知道你为什么要让你的所有类都继承自一个共同的“CAEntity”父级——代理和网格在概念上是完全不同的东西,没有有意义的共同行为或状态。
  • …另外,我看了你链接的论文,据我所知,他们描述的“细胞自动机”中的“细胞”根本没有相互作用.如果是这样,我只能说作者在一般的元胞自动机或随机过程方面显然一无所知,并且仅通过建模就可以以一小部分计算成本获得等效(但更准确)的结果他们的系统为三态Markov chain
  • 你说得对,这还不是全部代码。在六个类(抽象和继承)和结构中将有几百行。我想它会把东西卡在杂草里。我可以给出更详细的概述,但是...
  • 想象一个温度随季节变化的环境(图上的属性)。有陆地和水细胞(因此细胞的特性),并且在足够低的温度下,水细胞会冻结,因此动物代理可以使用它们在陆地位置之间穿越。想象一下那些动物代理人猎杀其他动物代理人吃它们(所以代理人的属性)。想象一下,被吃掉的动物代理会吃树(因此其他具有属性的代理),并且倾向于吃幼苗(限制树的生长),从而限制了它们自己的生长(以及食肉动物代理的生长)。
猜你喜欢
  • 2010-11-05
  • 1970-01-01
  • 2010-10-14
  • 2011-07-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多