Подтвердить что ты не робот

Разрешение итерации без создания мусора

У меня есть следующий код в пуле объектов, который реализует интерфейс IEnumerable.

public IEnumerable<T> ActiveNodes
{
    get
    {
        for (int i = 0; i < _pool.Count; i++)
        {
            if (_pool[i].AvailableInPool)
            {
                yield return _pool[i];
            }
        }
    }
}

Насколько я знаю (в соответствии с этим вопросом), это сгенерирует мусор, поскольку объект IEnumerable нужно будет собрать. Ни один из элементов в _pool никогда не будет собран, так как цель пула - сохранить ссылки на все их, чтобы предотвратить создание мусора.

Может ли кто-нибудь предложить способ разрешить итерацию через _pool, чтобы мусор не генерировался?

При повторении пула все элементы в пуле, которые имеют AvailableInPool == true, должны быть переименованы. Заказ не имеет значения.

4b9b3361

Ответ 1

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

Конструкция без мусора возможна путем возврата структур, которые не реализуют IEnumerable. Компилятор С# все еще может перебирать такие объекты, потому что оператор foreach использует утиную печать. Но опять же, маловероятно, что это поможет вам.

UPDATE

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

public struct StructEnumerable<T>
{
    private readonly List<T> pool;

    public StructEnumerable(List<T> pool)
    {
        this.pool = pool;
    }

    public StructEnumerator<T> GetEnumerator()
    {
        return new StructEnumerator<T>(this.pool);
    }
}

Вот StructEnumerator:

public struct StructEnumerator<T>
{
    private readonly List<T> pool;
    private int index;

    public StructEnumerator(List<T> pool)
    {
        this.pool = pool;
        this.index = 0;
    }

    public T Current
    {
        get
        {
            if (this.pool == null || this.index == 0)
                throw new InvalidOperationException();

            return this.pool[this.index - 1];
        }
    }

    public bool MoveNext()
    {
        this.index++;
        return this.pool != null && this.pool.Count >= this.index;
    }

    public void Reset()
    {
        this.index = 0;
    }
}

Вы можете просто вернуть StructEnumerable<T> следующим образом:

public StructEnumerable<T> Items
{
    get { return new StructEnumerable<T>(this.pool); }
}

И С# может перебирать это с помощью обычного foreach:

foreach (var item in pool.Items)
{
    Console.WriteLine(item);
}

Обратите внимание, что вы не можете LINQ над элементом. Для этого вам нужен интерфейс IEnumerable, который включает сбор бокса и мусора.

Удачи.

Ответ 2

Во-первых, многие люди отталкиваются от Ольховского, чтобы предположить, что это ни о чем не беспокоит. Избежание давления на входе действительно очень важно в некоторых приложениях в некоторых средах.

Компактный сборщик мусора содержит неисчерпаемую политику; он запускает сборку каждый раз, когда выделено 1000 КБ памяти. Теперь предположим, что вы пишете игру, которая работает на компактной основе, а физический движок генерирует 1 Кбайт мусора каждый раз, когда он запускается. Физические двигатели обычно работают порядка 20 раз в секунду. Так что 1200 КБ давления в минуту, и эй, что уже более одной коллекции в минуту просто от физического двигателя. Если сбор вызывает заметное заикание в игре, это может быть неприемлемым. В таком сценарии все, что вы можете сделать, чтобы уменьшить давление на сбор, помогает.

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

Итак, чтобы ответить на ваш вопрос, как вы можете перебирать коллекцию объединенных объектов, не создавая давления на сбор?

Во-первых, подумайте о том, почему в типичном сценарии происходит сбор давления. Предположим, что у вас есть

 foreach(var node in ActiveNodes) { ... }

Логически это выделяет два объекта. Во-первых, он выделяет перечислимую последовательность, которая представляет последовательность узлов. Во-вторых, он выделяет перечислитель - курсор - который представляет текущую позицию в последовательности.

На практике иногда вы можете обманывать бит и иметь один объект, который представляет как последовательность, так и перечислитель, но у вас все еще есть один выделенный объект.

Как мы можем избежать этого сбора давления? Приходят на ум три вещи.

1) Не используйте метод ActiveNodes в первую очередь. Сделать вызывающую программу итерацией по пулу по индексу и проверить, доступен ли node. Последовательность представляет собой пул, который уже выделен, и курсор является целым числом, ни одно из которых не создает новое давление в коллекции. Цена, которую вы платите, - это дублированный код.

2) Как предполагает Стивен, компилятор будет использовать любые типы, которые имеют правильные общедоступные методы и свойства; они не должны быть IEnumerable и IEnumerator. Вы можете создать свою собственную последовательность с изменяемой структурой и объекты курсора, передать их по значению и избежать сбора давления. Опасно иметь изменчивые структуры, но это возможно. Обратите внимание, что List<T> использует эту стратегию для своего счетчика; изучить его реализацию для идей.

3) Выделите последовательность и счетчики в куче как обычно и объедините их! У вас уже есть стратегия объединения, поэтому нет причин, по которым вы также не можете объединить счетчик. У счетчиков даже есть удобный метод Reset, который обычно просто генерирует исключение, но вы могли бы написать собственный объект перечислителя, который использовал его для reset перечислителя, в начале последовательности, когда он возвращается в пул.

Большинство объектов перечисляются только один раз за раз, поэтому в типичных случаях пул может быть небольшим.

(Теперь, конечно, у вас может быть проблема с курицей и яйцом, как вы собираетесь перечислять пул счетчиков?)

Ответ 3

Так как XNA для XBox также работает над Compact Framework (и я подозреваю, что вы работаете над заданными вами намеками (1)), мы можем доверяйте разработчикам XNA, чтобы научить нас точно, когда foreach создает мусор.

Чтобы процитировать наиболее важный момент (хотя вся статья стоит прочитать):

При выполнении foreach над Collection<T> должен быть назначен счетчик.

При выполнении foreach над большинством других коллекции, в том числе в виде массивов, списки, очереди, связанные списки и другие:

  • если коллекции используются явно, перечислитель НЕ будет выделены.
  • если они переданы в интерфейсы, будет назначен счетчик.

Итак, если _pool - это List, массив или аналогичный и может позволить себе, вы можете либо вернуть этот тип напрямую, либо применить IEnumerable<T> к соответствующему типу, чтобы избежать мусора во время foreach.

В качестве некоторого дополнительного чтения Shawn Hargreaves может быть полезной дополнительная информация.

(1) 60 вызовов в секунду, Compact Framework, не может перейти на собственный код, 1 МБ распределения до запуска GC.

Ответ 4

Должен ли он быть IEnumerable? Будет ли рефакторинг массив с хорошей старой проиндексированной асекшей?

Ответ 5

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

@Edit: если вы настроены на использование управляемой платформы и действительно хотите архивировать состояние нулевого мусора, откажитесь от использования пула в цикле foreach и повторите его по другому, возможно, используя индекс. Или подумайте о создании списка потенциальных узлов и верните это вместо этого.

@Edit2: Конечно, реализация IEnumerator и использование Current(), Next() и т.д. также будет работать.

Ответ 6

Вы можете реализовать свой собственный класс IEnumerator, который будет перечислять эти активные узлы.

Теперь, если вы можете гарантировать, что только один клиент будет перечислять по активным узлам в любой момент времени, ваш класс может кэшировать этот класс, поэтому существует только один экземпляр. Тогда никакой мусор не понадобится. Вызов ActiveNodes вызовет reset, чтобы начать перечисление с самого начала.

Это опасное предположение, но если вы оптимизируете, вы можете рассмотреть его

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

Ответ 7

Кроме того, у вас может быть пул предварительно распределенных счетчиков. Подумайте, сколько одновременных перечислений вы хотите поддержать.

Накладные расходы на сбор мусора будут идти за счет дополнительного потребления памяти. Диплом оптимизации скорости и памяти в ее чистом виде.