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

У меня есть следующий код в пуле объектов, который реализует интерфейс 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, должны быть перебраны. Порядок не имеет значения.


person Olhovsky    schedule 30.03.2011    source источник
comment
Отходы — это факт жизни. Хотя мы не должны генерировать чрезмерные потери, неразумно стараться изо всех сил генерировать отсутствие потерь вообще (и да, это относится и к C#).   -  person SWeko    schedule 30.03.2011
comment
Я не вижу явной проблемы с мусором в вашем коде... вы обнаружили (используя профилировщик), что этот фрагмент кода является проблемой?   -  person digEmAll    schedule 30.03.2011
comment
что вы подразумеваете под «необходимо собрать объект IEnumerable»? ваш _pool[i] просто возвращается как IEnumerable. На него потом ничего не собрано?   -  person user492238    schedule 30.03.2011
comment
Никакой сборки мусора не произойдет, пока у вас не будет ссылки на класс, содержащий это поле _pool.   -  person Darin Dimitrov    schedule 30.03.2011
comment
(Упс, не видел этих комментариев выше .. ;))   -  person user492238    schedule 30.03.2011
comment
@SWeko +1 за это хорошее замечание. Самый важный рег. .NET - это IMO, чтобы мусор был небольшим (только для Gen1). Гораздо больше, чем избегание этого.   -  person user492238    schedule 30.03.2011
comment
Комментаторы: см. предыдущий вопрос ОП здесь (stackoverflow.com/questions/5486363/), на который был дан плохой ответ и, вероятно, объясняет этот вопрос...   -  person Dan Puzey    schedule 30.03.2011
comment
Я думаю, он хочет избежать объекта IEnumerable... или, возможно, перечислителя. Почему я не знаю.   -  person Skurmedel    schedule 30.03.2011
comment
@user492238 user492238 В компактной CLR .NET сохранение небольшого размера мусора (т. Е. Небольшой глубины) на самом деле является незначительной разницей. Мне нужно вообще избегать мусора.   -  person Olhovsky    schedule 30.03.2011
comment
@Skurmedel: потому что я использую компактную среду CLR .NET, которая использует совершенно другой подход к сборке мусора, чем более надежная настольная версия интерпретатора.   -  person Olhovsky    schedule 30.03.2011
comment
@Darin: ссылка на класс, содержащий поле _pool, никогда не будет потеряна, поэтому там никогда не будет происходить сборка мусора. Разве IEnumerable не создает мусор?   -  person Olhovsky    schedule 30.03.2011
comment
@SWeko: я не пытаюсь полностью избежать потерь, я просто пытаюсь избежать потерь в этом конкретном фрагменте кода.   -  person Olhovsky    schedule 30.03.2011
comment
@Оловский: Хорошо. Спасибо за разъяснения. Хотя было бы неплохо указать в вопросе;)   -  person Skurmedel    schedule 30.03.2011
comment
@Dan Puzey: Что заставляет вас говорить, что на этот другой вопрос ответили плохо?   -  person Olhovsky    schedule 30.03.2011
comment
@Olhovsky: ну, если вы хотите избежать создания экземпляра объекта IEnumerable<>, вы можете скопировать этот фрагмент кода (удалив yield) куда угодно (но, очевидно, это может привести к большому дублированию кода)   -  person digEmAll    schedule 30.03.2011
comment
@Olhovsky: на мой взгляд, обращение к IEnumerable как к мусору в лучшем случае вводит в заблуждение. Это не одноразовый тип, это не вызовет проблем в приложении, не о чем беспокоиться. Это не больше мусора, чем целое число, которое вы используете в своем цикле.   -  person Dan Puzey    schedule 30.03.2011
comment
@Dan: Ну, это это больше мусора, чем целое число, используемое в цикле, поскольку целое число никогда не вызовет сбор, а IEnumerable потенциально вызовет сбор, когда на него больше ничего не ссылается.   -  person Olhovsky    schedule 31.03.2011


Ответы (7)


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

Дизайн без мусора возможен за счет возврата структур, которые не реализуют IEnumerable. Компилятор C# по-прежнему может выполнять итерацию таких объектов, поскольку оператор foreach использует утиную типизацию. .NET List<T>, например, использует этот подход.

При использовании foreach как для массива, так и для List<T> мусор не будет генерироваться. При использовании foreach для массива C# преобразует операцию в оператор for, в то время как List<T> уже реализует перечислитель struct, в результате чего foreach не создает мусора.

Вот перечисляемая структура и перечислитель структур. Когда вы возвращаете перечислимое, компилятор C# может выполнить foreach по нему:

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

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

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

Обратите внимание, что вы не можете использовать LINQ для элемента с помощью System.Linq.Enumerable. Для этого вам нужен интерфейс IEnumerable<T>, а это включает в себя создание перечислителей и, следовательно, сборку мусора. Вы можете, конечно, создать свои собственные методы расширения LINQ, но это вряд ли поможет, потому что это все равно будет приводить к созданию новых объектов (когда замыкания создаются для используемых делегатов).

person Steven    schedule 30.03.2011
comment
Тогда мой сценарий особенный, и предотвращение создания мусора имеет решающее значение для моего приложения, которое работает в компактной среде .NET CLR, которая имеет совсем другой подход к сборке мусора, чем интерпретатор рабочего стола. - person Olhovsky; 30.03.2011
comment
Вызов ActiveNodes приведет к созданию одного объекта. Это немного для сборщика мусора. Даже для фреймворка Compact. - person Steven; 30.03.2011
comment
@Steven: В моем случае это 60 объектов в секунду на пул, умноженное на множество пулов. Компактная среда CLR выполняет сбор после каждого выделенного 1 МБ. Задержка, вызванная коллекцией, значительна. - person Olhovsky; 30.03.2011
comment
Вы отредактировали свой ответ, упомянув возвращаемые структуры, которые, если это возможно, могут быть решением моего вопроса. Но вы не упомянули, как я могу вернуть структуру или что она будет делать :) - person Olhovsky; 30.03.2011
comment
@Steven: Скажи мне, как вернуть структуру, которая разрешает итерацию без реализации IEnumerable, и ты получишь галочку :) - person Olhovsky; 30.03.2011

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

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

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

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

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

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

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

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

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

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

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

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

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

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

person Eric Lippert    schedule 30.03.2011
comment
«Если коллекция вызывает заметное зависание в игре», то игра все равно иногда будет лагать, верно? Значит, если мы не можем избежать ЛЮБОЙ коллекции, CLR просто не подойдет для реализации игры? - person user492238; 30.03.2011
comment
@ user492238: У разработчиков игр есть множество методов для минимизации, смягчения или устранения пауз, вызванных сборкой мусора в играх. Достаточно ли этих методов для гейм-дизайнера, чтобы создать играбельную и прибыльную игру на какой-либо версии CLR, — это не та тема, которую можно легко осветить в пятистах символах комментариев. Тот факт, что многие люди делают создание игр для различных версий среды CLR, свидетельствует о том, что она подходит для многих игр. - person Eric Lippert; 30.03.2011
comment
Спасибо. Не собирался возражать против какой-либо из этих техник. Я успешно объединяю себя много. Но для приложения, которое на 100% не содержит мусора, я считаю сборщик мусора своего рода слишком дорогим «менеджером резервной памяти» (технически говоря). Неважно ;) - person user492238; 30.03.2011
comment
Спасибо, Эрик, этот ответ был полезен :) - person Olhovsky; 30.03.2011
comment
@EricLippert эй, в критических ситуациях я обычно использую '.ToArray()' всякий раз, когда массив изменяется (или один раз за итерацию максимум), а затем перебираю массив с помощью 'for' вместо 'foreach', что вы думаете об этом? - person cubrman; 24.08.2015

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

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

При выполнении foreach над Collection<T> перечислитель БУДЕТ выделен.

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

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

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

В качестве дополнительного чтения, Шон Харгривз может предоставить полезную дополнительную информацию.

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

person Kyte    schedule 31.03.2011

Должен ли он быть IEnumerable? Поможет ли рефакторинг в массив со старым добрым индексированным acecss?

person Seva Alekseyev    schedule 30.03.2011
comment
Это не обязательно должен быть IEnumerable. Я просто хочу повторить. Думаю, я могу написать индексатор. Моя надежда на этот вопрос заключалась в том, что я мог бы услышать несколько разных вариантов, чтобы я мог лучше решить, что использовать. Вместо этого дискуссия выродилась в том, что мне приходится объяснять всем, почему я избегаю мусора. - person Olhovsky; 30.03.2011
comment
Перечислитель — это просто способ абстрагироваться от понятия текущей позиции в последовательности. Если последовательность допускает произвольный доступ O(1), целочисленная переменная подойдет точно так же. - person Seva Alekseyev; 30.03.2011
comment
Ну, технически индексирование потенциально будет немного медленнее (я думаю), так как мне придется каждый раз создавать новый индекс перед выполнением итерации для поиска следующего индекса. - person Olhovsky; 30.03.2011
comment
Индекс — это целочисленная переменная в стеке. Его создание занимает нулевое время, так как код выделения кадра стека уже есть. - person Seva Alekseyev; 30.03.2011
comment
Seva: Для того, чтобы найти элемент, который нужно вернуть, по заданному индексу необходимо выполнить цикл по массиву, пока не будет найден следующий узел со свойством AvailableInPool, установленным в true. Таким образом, будет выделена новая переменная для подсчета цикла при каждом доступе. Вот почему использование индексатора будет немного медленнее (если только компилятор не проделает здесь какую-то магию, но с компактной структурой это очень маловероятно, и маловероятно с надежной структурой). - person Olhovsky; 30.03.2011
comment
Все сказанное, поскольку в этом случае альтернативой является создание структуры и вызов методов в ней (которые никогда не встраиваются в компактную структуру), я не уверен, что будет быстрее. Я думаю, я могу попробовать и посмотреть. - person Olhovsky; 30.03.2011

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

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

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

person user492238    schedule 30.03.2011
comment
Как я уже сказал в вопросе, цель пула состоит в том, чтобы предотвратить удаление элементов в _pool, поэтому для них нет затрат на сборку мусора. Крошечные мусорные объекты маленькие, но их много. Этот итератор вызывается много раз в секунду несколькими экземплярами этого класса. Я знаю, что использование итератора, возможно, является решением. Весь смысл вопроса в том, КАК это может быть решением? - person Olhovsky; 30.03.2011
comment
@Olhovsky «многие» объекты не наносят вреда GC. Как это работает, он считает все объекты мусором и собирает только живые объекты. КАК может выглядеть решение? Возможно, реализуя функцию GetNode(), которая возвращает следующий доступный узел (если есть). Вызовите эту функцию повторно - без foreach. - person user492238; 30.03.2011
comment
@ user492238: В компактной среде CLR .NET сборка мусора выполняется один раз на каждый 1 МБ созданного мусора. Таким образом, частота сбора сборщиком мусора зависит исключительно от того, сколько объектов в единицу времени создается этим методом, при условии, что все мусорные объекты, созданные этим методом, имеют одинаковый размер. - person Olhovsky; 30.03.2011

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

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

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

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

person Mongus Pong    schedule 30.03.2011
comment
На самом деле вам не нужно 2 клиента, чтобы нарушить предположение: достаточно 2 вложенных foreach на одном и том же IEnumerable... - person digEmAll; 30.03.2011

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

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

person Seva Alekseyev    schedule 31.03.2011