"Закрытие по переменной дает немного худшую производительность". Как?

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

  • Как будет закрытие?
  • Как это повлияет на производительность?

Вот question

List.Where(s => s.ValidDate.Date == DateTime.Today.Year).ToList();

Вот мое решение . Я ввел переменную yr для хранения года.

int yr = DateTime.Now.Year;
List.Where(s => s.ValidDate.Year == yr).ToList();

Вот он в ответе .

Ответ 1

Прежде всего, эти два решения не являются функционально эквивалентными (если вы исправили сравнение даты с int (.Date == .Today.Year)):

  • Первый фрагмент переопределяет DateTime.Today.Year для каждого значения списка, который может давать разные результаты, когда текущий год изменяется во время итерации

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

Закрытие введено, поскольку лямбда обращается к переменной из ее внешней области, она закрывается над значением yr. Компилятор С# будет генерировать новый класс с полем, содержащим yr. Все ссылки на yr будут заменены новым полем, а исходный yr даже не будет существовать в скомпилированном коде

Я сомневаюсь, что будет штраф за выполнение, введя закрытие. Если есть, код с использованием закрытия будет быстрее, так как ему не нужно создавать новые экземпляры DateTime для каждого элемента списка, а затем разыменовывать два свойства. Он должен иметь доступ только к полю класса, сгенерированного компилятором, который содержит значение int текущего года. (Любой, кто хочет сравнить сгенерированный код или профиль IL, два фрагмента?:))

Ответ 2

В дополнение к ответу knittl я хотел попробовать и измерить производительность с закрытием и без него, вот как выглядит мой тест:

internal class SomeData {
    public DateTime ValidDate { get; set; }
    // other data ...
}

class Program {
    static void Main(string[] args) {
        var stopWatch = new Stopwatch();

        // Test with closure
        IEnumerable<SomeData> data1 = CreateTestData(100000);
        stopWatch.Start();
        int yr = DateTime.Now.Year;
        List<SomeData> results1 = data1.Where(x => x.ValidDate.Year == yr).ToList();
        stopWatch.Stop();
        Console.WriteLine("With a closure - {0} ms", stopWatch.Elapsed.Milliseconds);
        // ### Output on my machine (consistently): With a closure - 16 ms

        stopWatch.Reset();

        // Test without a closure            
        IEnumerable<SomeData> data2 = CreateTestData(100000);
        stopWatch.Start();
        List<SomeData> results2 = data2.Where(x => x.ValidDate.Year == DateTime.Today.Year).ToList();
        stopWatch.Stop();
        Console.WriteLine("Without a closure - {0} ms", stopWatch.Elapsed.Milliseconds);
        // ### Output on my machine: Without a closure - 33 ms
    }

    private static IEnumerable<SomeData> CreateTestData(int numberOfItems) {
        var dt = DateTime.Today;
        for (int i = 0; i < numberOfItems; i++) {
            yield return new SomeData {ValidDate = dt};
        }
    }
}

Нижняя строка моих тестов - как я ожидал, версия с закрытием значительно быстрее.

Ответ 3

Здесь наивное измерение времени, просто чтобы дополнить ответ knittl.

В результате версия, которая оценивает DateTime.Now каждый раз, более чем в 10 раз медленнее, чем ваш код.

Результаты на моей машине: T1: 8878 мс; T2: 589 мс. (Максимальная оптимизация, без отладчика и т.д.).

class Program
{
    static void Main(string[] args)
    {
        var things = new List<Something>();
        var random = new Random(111);
        for (int i = 0; i < 100000; ++i)
        {
            things.Add(new Something(random.Next(2010, 2016)));
        }

        // to avoid measuring the JIT compilation and optimization time
        T1(things);
        T2(things);

        var sw = Stopwatch.StartNew();
        for (int i = 0; i < 100; ++i)
        {
            T1(things);
        }
        Console.WriteLine(sw.ElapsedMilliseconds);
        sw.Restart();
        for (int i = 0; i < 100; ++i)
        {
            T2(things);
        }
        Console.WriteLine(sw.ElapsedMilliseconds);

        Console.ReadLine();
    }

    private static void T1(List<Something> list)
    {
        var result = list.Where(x => x.ValidDate.Year == DateTime.Now.Year).ToList();
    }

    private static void T2(List<Something> list)
    {
        var yr = DateTime.Now.Year;
        var result = list.Where(x => x.ValidDate.Year == yr).ToList();
    }
}

class Something
{
    public Something(int year)
    {
        this.ValidDate = new DateTime(year, 1, 1);
    }

    public DateTime ValidDate { get; private set; }
}