Почему компилятор не может оптимизировать переменную замыкания путем встраивания? - PullRequest
0 голосов
/ 30 августа 2018

У меня есть Main метод, подобный этому:

static void Main(string[] args)
{
     var b = new byte[1024 * 1024];

     Func<double> f = () =>
     {
         new Random().NextBytes(b);
         return b.Cast<int>().Average();
     };

     var avg = f();
     Console.WriteLine(avg);
}

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

Что мне интересно, так как я не обращаюсь к b и не изменяю его где-либо после объявления Func, почему компилятор не может встроить эту локальную переменную и не беспокоиться о создании класса? Как это:

Func<double> f = () =>
{
    var b = new byte[1024 * 1024];
    new Random().NextBytes(b);
    return b.Cast<int>().Average();
};

Я скомпилировал этот код в режимах Debug и Release, DisplayClass генерируется в обоих:

enter image description here

Это просто не реализовано как оптимизация или мне чего-то не хватает?

1 Ответ

0 голосов
/ 30 августа 2018

Это просто не реализовано как оптимизация или мне чего-то не хватает?

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

Тем не менее, более общий смысл уместен. Существует множество оптимизаций, которые вы можете выполнить, если знаете, что закрытая переменная используется только для ее значения.

Когда я был в команде компиляторов - я ушел в 2012 году - Нил Гафтер и я рассмотрел возможность реализации таких оптимизаций, а также ряд более сложных оптимизаций, предназначенных для уменьшения вероятности слишком долгого продления срока службы дорогого объекта. случайно.

В сторону: Самый простой из более сложных сценариев: у нас есть две лямбды, преобразованные в делегатов; один хранится в недолговечной переменной и закрывается над локальной переменной, содержащей ссылку на дорогой объект; один хранится в долгоживущей переменной и закрывается над локальной переменной, которая ссылается на дешевый объект. Дорогой объект живет столько же, сколько долгоживущая переменная, даже если он не используется. В более общем смысле, множественные замыкания могут быть построены как раздел на основе закрытого отношения; в то время мы только разделяли замыкания на основе вложенности; замыкания на том же уровне вложенности были одним замыканием. Данный сценарий встречается редко и есть очевидные обходные пути, но было бы неплохо, если бы его вообще не было.

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

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

Я не знаю, были ли эти оптимизации реализованы за это время; скорее всего нет.

Я также не знаю, выполняет ли компилятор такие оптимизации для локальных функций C # 7, хотя я подозреваю, что ответ "да". Посмотрите, что произойдет, если вы попробуете локальную функцию!

...