C# Проблемы памяти - PullRequest
       45

C# Проблемы памяти

1 голос
/ 19 февраля 2020

Я работаю с сотовыми автоматами . Мой репозиторий для моей работы здесь . Базовая структура c имеет вид

1) Сетка из 2) ячеек, в которой может быть 3) агента.

Агенты действуют в соответствии с набором правил, и обычно один из них обозначает " состояния "для агентов (агенты разных государств имеют разные правила). Один (относительно) хорошо известный CA - это игра жизни .

Я пытаюсь немного расширить возможности и включить другие типы «свойств» в мои 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;
    }
}

(примечание: ранее у меня была переменная "type" для точного набора текста во время выполнения ... но в попытках решить проблему в этом посте я удалил ее. Факт это нужно добавить обратно)

Это хорошо. Тем не менее, я пытаюсь сделать некоторые работы с сеткой больших размеров. 100x100. 1000x1000. 5000x5000, или 25 миллионов клеток (и агентов). Это будет 25 миллионов словарей.

Visual Studio memory snapshot

См. Изображение: снимок памяти из Visual Studio для сетки 4000x4000 или 16 миллионов агентов (я пытался 5000x5000, но Visual Studio не позволила бы мне сделать снимок).

Справа хорошо видно, что отладчик считывает использование памяти 8 ГБ (и я попробовал это в версии выпуска, чтобы увидеть 6875 Использование МБ). Однако при подсчете всего в третьем столбце снимка я получаю менее 4 ГБ.

Почему существует такое драматическое c расхождение между общим использованием памяти и размером сохраненных объектов в памяти?

Дополнительно: как я могу оптимизировать использование памяти (в основном это словари - есть ли другая коллекция с похожим поведением, но меньшим использованием памяти)?

Редактировать: Для каждого из трех "компонентов "(Сетка, Клетка, Агент) У меня есть класс. Все они наследуются от исходного класса 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;
        }
    }

1 Ответ

1 голос
/ 20 февраля 2020

Похоже, ваша проблема в том, что ваше представление ваших агентов (с использованием словарей) занимает слишком много памяти. Если это так, решение состоит в том, чтобы найти более компактное представление.

Поскольку вы работаете на объектно-ориентированном языке, типичным решением будет определение класса агента, возможно с подклассами для различных типов агентов. и используйте переменные экземпляра для хранения состояния каждого агента. Тогда ваша сетка CA будет массивом экземпляров Агента (или, возможно, пустыми для свободных ячеек). Это будет намного более компактно, чем использование словарей со строковыми ключами.

Кроме того, я бы рекомендовал не хранить положение агента в сетке как часть состояния агента, но передавать это как параметр для любых методов, которые нуждаются в этом. Это не только экономит немного памяти само по себе, но также позволяет размещать ссылки на один и тот же экземпляр агента в нескольких ячейках сетки, чтобы представлять несколько идентичных агентов. В зависимости от того, как часто такие идентичные агенты встречаются в вашем ЦС, это может сэкономить огромный объем памяти.

Обратите внимание, что если вы изменяете состояние такого повторно используемого экземпляра агента, модификация очевидно, повлияет на всех агентов в сетке, представленной этим экземпляром. По этой причине может быть хорошей идеей сделать объекты агента неизменяемыми и просто создавать новые при каждом изменении состояния агента.

Вы могли бы также захотеть сохранить кэш ( например, набор) экземпляров Агента, уже находящихся в сетке, чтобы вы могли легко проверить, идентичен ли новый агент существующему. Будет ли это действительно полезно, зависит от вашей конкретной модели c CA - с некоторым CA вы могли бы достаточно хорошо справляться с дедупликацией даже без такого кэша (вполне нормально иметь дубликат some Объекты-агенты), в то время как для других может просто не хватить идентичных агентов, чтобы сделать это стоящим. Кроме того, если вы попробуете это, обратите внимание, что вам нужно либо спроектировать кеш, чтобы использовать слабые ссылки (что может быть сложно, чтобы получить правильные данные), либо периодически очищать и перестраивать его, чтобы избежать старых объектов Агента, задерживающихся в кеше, даже после того, как они были удалены из таблицы.


Приложение на основе вашего комментария ниже, который я процитирую здесь:

Представьте себе среду, в которой температура меняется в зависимости от сезона (так что свойство на графике). Есть клетки земли и воды (так что свойства на клетках), и при достаточно низких температурах клетки воды замерзают, так что агенты животных могут использовать их для пересечения между участками земли. Представьте, что эти агенты животных охотятся на других агентов животных, чтобы съесть их (поэтому свойства агентов). Представьте, что животные агенты, которых едят, едят деревья (то есть другие агенты, обладающие свойствами) и, как правило, едят молодые саженцы (ограничивая рост деревьев), тем самым ограничивая их собственный рост (и рост агентов плотоядных).

Хорошо, давайте набросаем классы, которые вам понадобятся. (Пожалуйста, извините за любые синтаксические ошибки; я на самом деле не C# программист, и я на самом деле не тестировал этот код. Просто подумайте о нем как о C# -подобном псевдокоде или о чем-то.)

Сначала все, вам, очевидно, понадобится куча агентов. Давайте определим абстрактный суперкласс для них:

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

Наша CA-симуляция (для простоты я собираюсь предположить, что она стохастична c, то есть та, где агенты действуют по одному в случайный порядок, как в алгоритме Gillesp ie ), в основном будет включать многократный выбор случайной ячейки ( 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 полностью лишился состояния, я пошел дальше и использовал один и тот же глобальный экземпляр Tree для всех деревьев в сетке! Хотя это не всегда возможно, но когда это так, это может сэкономить много памяти.

Теперь, как насчет сетки? Базовая реализация c (на мгновение игнорируя различные типы ячеек) выглядела бы примерно так:

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 l oop. По своей сути c это может выглядеть так:

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

(Технически, чтобы правильно реализовать алгоритм Gillesp ie, приращение времени моделирования между итерациями должно составлять , экспоненциально распределенное случайное число со средним dt, не постоянным приращением. Однако, поскольку в каждой итерации выбирается только один субъект из одной из width * height ячеек, количество итераций между действиями одного и того же действующего лица - это , геометрически распределенное со средним width * height, и умножение его на dt = 1 / (width * height) дает превосходное приближение для экспоненциального распределения со средним 1. Что является многословным способом сказать, что на практике использование шаг с постоянным временем - это прекрасно.)

Поскольку это становится достаточно длинным, я позволю вам продолжить отсюда. Я просто отмечу, что существует множество способов дальнейшего расширения и / или оптимизации алгоритма, который я набросал выше.

Например, вы можете ускорить симуляцию, сохранив список всех местоположений сетки, которые содержит живого актера и случайную выборку актеров из этого списка (но тогда вам также необходимо масштабировать временной шаг обратно пропорционально длине списка). Кроме того, вы можете решить, что хотите, чтобы у одних актеров больше шансов действовать, чем у других; в то время как простой способ сделать это - просто использовать выборку отклонения (то есть, чтобы актер делал что-то, только если rng.Sample() < prob для некоторых prob между 0 и 1), более эффективный способ состоял бы в том, чтобы поддерживать несколько списков местоположений в зависимости от тип актера там.

...