Локальные функции в С# - захватить или не захватить при передаче параметров вниз?

При использовании локальных функций в С# 7 у вас есть две опции, когда вы хотите передать параметры (или другие локальные переменные) из основного метода до локальной функции: вы можете явно объявить параметры так же, как и любую другую функцию, или просто "захватить" параметры/переменные из содержащего метода и использовать их напрямую.

Пример, возможно, иллюстрирует это лучше всего:

Явное объявление

public int MultiplyFoo(int id)
{
    return LocalBar(id);

    int LocalBar(int number)
    {
        return number * 2;
    }
}

Захватив

public int MultiplyFoo(int id)
{
    return LocalBar();

    int LocalBar()
    {
        return id * 2;
    }
}

Оба метода работают одинаково, но способ, которым они ссылаются на локальную функцию, отличается.

Поэтому мой вопрос:

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

Ответы

Ответ 1

Локальные функции на С# являются умными с точки зрения их захвата - по крайней мере, в реализации Roslyn. Когда компилятор может гарантировать, что вы не создаете делегата из локальной функции (или делаете что-то еще, что продлит время жизни переменной), он может использовать параметр ref со всеми захваченными переменными в сгенерированной структуре для связи с локальной функцией. Например, ваш второй метод будет выглядеть примерно так:

public int MultiplyFoo(int id)
{
    __MultiplyFoo__Variables variables = new __MultiplyFoo__Variables();
    variables.id = id;
    return __Generated__LocalBar(ref variables);
}

private struct __MultiplyFoo__Variables
{
    public int id;
}

private int __Generated__LocalBar(ref __MultiplyFoo__Variables variables)
{
    return variables.id * 2;
}

Таким образом, не требуется выделение кучи, так как было бы (скажем) выражение лямбда, преобразованное в делегат. С другой стороны, есть построение структуры, а затем копирование значений в это. Является ли передача int по значению более или менее эффективной, чем передача структуры по ссылке, вряд ли будет значимой... хотя я предполагаю, что в случаях, когда у вас была огромная структура как локальная переменная, это означало бы, что использование неявного захвата будет более эффективным, чем использование простого параметра значения. (Аналогично, если ваша локальная функция использовала множество захваченных локальных переменных).

Ситуация уже усложняется, когда несколько локальных переменных захватываются различными локальными функциями, и тем более, когда некоторые из них являются локальными функциями в циклах и т.д. Изучение с помощью ildasm или Reflector и т.д. Может быть довольно интересным.

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

Ответ 2

Это был интересный вопрос. Сначала я декомпилировал вывод сборки.

public int MultiplyFoo(int id)
{
  return LocalFunctionTests.\u003CMultiplyFoo\u003Eg__LocalBar\u007C0_0(id);
}

public int MultiplyBar(int id)
{
  LocalFunctionTests.\u003C\u003Ec__DisplayClass1_0 cDisplayClass10;
  cDisplayClass10.id = id;
  return LocalFunctionTests.\u003CMultiplyBar\u003Eg__LocalBar\u007C1_0(ref cDisplayClass10);
}

Когда вы передаете id как параметр, локальная функция вызывается с переданным параметром id. Ничего необычного, и параметр хранится в кадре стека метода. Однако, если вы не передадите этот параметр, структура (мысль с именем "класс", как указала Дайзи) создается с помощью поля (cDisplayClass10.id = id), и ему присваивается идентификатор. Затем структура передается в качестве ссылки в локальную функцию. Компилятор С#, похоже, делает это для поддержки закрытия.

По производительности я использовал Stopwatch.ElapsedTicks, передавая id как параметр, был последовательно быстрее. Я думаю, это из-за стоимости создания структуры с полем. Разница в производительности расширилась, когда я добавил еще один параметр к локальной функции.

  • Идентификатор прохождения: 2247
  • Непревзойденный Id: 2566

Это мой код тестов, если кто-то заинтересован

public int MultiplyFoo(int id, int id2)
{
    return LocalBar(id, id2);

    int LocalBar(int number, int number2)
    {
        return number * number2 * 2;
    }
}

public int MultiplyBar(int id, int id2)
{
    return LocalBar();

    int LocalBar()
    {
        return id * id2 * 2;
    }
}


[Fact]
public void By_Passing_Id()
{
    var sut = new LocalFunctions();

    var watch = Stopwatch.StartNew();
    for (int i = 0; i < 10000; i++)
    {
        sut.MultiplyFoo(i, i);
    }

    _output.WriteLine($"Elapsed: {watch.ElapsedTicks}");
}

[Fact]
public void By_Not_Passing_Id()
{
    var sut = new LocalFunctions();

    var watch = Stopwatch.StartNew();
    for (int i = 0; i < 10000; i++)
    {
        sut.MultiplyBar(i, i);
    }

    _output.WriteLine($"Elapsed: {watch.ElapsedTicks}");
}