Почему это лямбда-замыкание создает мусор, хотя он не выполняется во время выполнения?

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

private Dictionary<Type, Action> actionTable = new Dictionary<Type, Action>();

private void Update(int num)
{
    Action action;
//  if (!actionTable.TryGetValue(typeof(int), out action))
    if (false)
    {
        action = () => Debug.Log(num);
        actionTable.Add(typeof(int), action);
    }
    action?.Invoke();
}

Я понимаю, что использование лямбда, такого как () => Debug.Log(num), сгенерирует небольшой вспомогательный класс (например, <> c__DisplayClass7_0) для хранения локальной переменной. Вот почему я хотел проверить, могу ли я кэшировать это выделение в словаре. Однако я заметил, что вызов Обновлять приводит к выделению памяти, даже если лямбда-код никогда не достигается из-за оператора if. Когда я закомментирую лямбду, выделение исчезает из профилировщика. Я использую инструмент Unity Profiler (инструмент отчетности о производительности в игровом движке Unity), который показывает такие распределения в байтах на кадр в режиме разработки / отладки.

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

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

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

Я не знаком с термином «распределение сборщика мусора». Есть выделение кучи и сборка мусора. Вы имели в виду одно из этих или что-то еще? Как именно вы определяете, что что-то выделено в вашем коде (т.е. какой инструмент вы используете и что вы видите)?

John Wu 11.12.2018 22:09

У вас отключена оптимизация? Я удивлен, что есть какой-либо код, связанный с лямбдой, поскольку он находится под if (false), который будет полностью удален оптимизатором. Если вы запускаете это с выключенным оптимизатором, что ж, вам не следует ожидать оптимизированного кода при выключении оптимизатора!

Eric Lippert 11.12.2018 22:42

Хороший замечание об оптимизаторе, но я опубликовал закомментированную строку, потому что хотел показать, что в моем фактическом коде я использую словарь, который изменяется во время выполнения, и, следовательно, я предполагаю, что оператор if все равно не будет оптимизирован . У меня сейчас нет инструментов для проверки этих конкретных выделений в режиме выпуска, хотя было бы интересно проверить.

Xarbrough 11.12.2018 23:07

См. Также stackoverflow.com/q/3885106/18192.

Brian 12.12.2018 20:45
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
4
396
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

I surmise that the compiler or JIT compiler generates the helper class for the lambda for the scope of the method even though I don't understand why this would be desirable.

Рассмотрим случай, когда существует более одного анонимного метода с замыканием в одном методе (достаточно частое явление). Вы хотите создать новый экземпляр для каждого из них или просто сделать так, чтобы все они использовали один экземпляр? Они пошли с последним. У любого подхода есть свои преимущества и недостатки.

Finally, is there any way of caching delegates in this manner without allocating and without forcing the calling code to cache the action in advance?

Просто переместите этот анонимный метод в его собственный метод, чтобы анонимный метод когда этот метод вызывается создавался безоговорочно.

private void Update(int num)
{
    Action action = null;
    //  if (!actionTable.TryGetValue(typeof(int), out action))
    if (false)
    {
        Action CreateAction()
        {
            return () => Debug.Log(num);
        }
        action = CreateAction();
        actionTable.Add(typeof(int), action);
    }
    action?.Invoke();
}

(Я не проверял, произошло ли выделение для вложенного метода. Если да, сделайте его невложенным методом и передайте int.)

Ответ Серви правильный и дает хорошее решение. Я подумал, что могу добавить еще несколько деталей.

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

Если у вас есть закрытая внешняя переменная лямбда, тогда все закрытые переменные превращаются в поля класса закрытия, и этот класс закрытия выделяется из долгосрочного пула («куча»), как только функция активирован. Это происходит несмотря ни на что того, читается ли когда-либо класс закрытия.

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

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

  • Сценарий с вероятностью 99,99% для блока итератора (метод с yield return в нем) состоит в том, что IEnumerable будет иметь GetEnumerator с именем ровно один раз. Таким образом, сгенерированный перечислимый объект имеет логику, которая реализует как IEnumerable, так и IEnumerator; время первый вызывается GetEnumerator, объект преобразуется в IEnumerator и возвращается. Время второй, мы выделяем второй счетчик. Это сохраняет один объект в наиболее вероятном сценарии, а сгенерированный дополнительный код довольно прост и редко вызывается.
  • Для методов async характерно иметь «быстрый путь», который возвращается без ожидания - например, у вас может быть дорогостоящий асинхронный вызов в первый раз, а затем результат кэшируется и возвращается во второй раз. Компилятор C# генерирует код, который избегает создания замыкания «конечного автомата» до тех пор, пока не встретится первый await, и, следовательно, предотвращает выделение памяти на быстром пути, если таковое имеется.

Эти оптимизации имеют тенденцию окупаться, но в 99% случаев, когда у вас есть метод, который выполняет закрытие, он фактически выполняет закрытие. На самом деле не стоит откладывать это.

Другие вопросы по теме