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

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

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

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

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

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

Ответы [ 2 ]

0 голосов
/ 12 декабря 2018

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

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

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

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

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

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

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

0 голосов
/ 12 декабря 2018

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

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

Наконец, есть ли способ кэширования делегатов таким способом без выделения и без принудительного вызова вызывающего кода для предварительного кэширования действия?

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

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.)

...