Кэширует ли LINQ вычисленные значения?

Предположим, у меня есть следующий код:

var X = XElement.Parse (@"
    <ROOT>
        <MUL v='2' />
        <MUL v='3' />
    </ROOT>
");
Enumerable.Range (1, 100)
    .Select (s => X.Elements ()
        .Select (t => Int32.Parse (t.Attribute ("v").Value))
        .Aggregate (s, (t, u) => t * u)
    )
    .ToList ()
    .ForEach (s => Console.WriteLine (s));

Что на самом деле делает среда выполнения.NET? Это анализ и преобразование атрибутов в целые числа каждый из 100 раз, или это достаточно умен, чтобы выяснить, что он должен кэшировать проанализированные значения и не повторять вычисления для каждого элемента в диапазоне?

Более того, как бы мне самому разобраться в этом?

Заранее спасибо за помощь.

2 ответа

Решение

Прошло много времени с тех пор, как я копался в этом коде, но, IIRC, путь Select работает просто кешировать Func вы поставляете его и запускаете его в исходной коллекции по одному. Таким образом, для каждого элемента во внешнем диапазоне он будет запускать внутренний Select/Aggregate последовательность, как будто это был первый раз. Никакого встроенного кэширования не происходит - вам придется реализовать это самостоятельно в выражениях.

Если вы хотите понять это самостоятельно, у вас есть три основных варианта:

  1. Скомпилируйте код и используйте ildasm просматривать IL; это наиболее точно, но, особенно с лямбдами и замыканиями, то, что вы получаете от IL, может выглядеть совсем не так, как в компиляторе C#.
  2. Используйте что-то вроде dotPeek для декомпиляции System.Linq.dll в C#; Опять же, то, что вы получаете от этих видов инструментов, может только приблизительно напоминать исходный исходный код, но по крайней мере это будет C# (и dotPeek, в частности, довольно неплохо работает и бесплатен).
  3. Мои личные предпочтения - скачайте справочный источник.NET 4.0 и поищите сами; это то, для чего это нужно:) Вы должны просто доверять MS, что эталонный источник соответствует фактическому источнику, используемому для создания двоичных файлов, но я не вижу веских оснований сомневаться в них.
  4. Как указывает @AllonGuralnek, вы можете устанавливать точки останова для определенных лямбда-выражений в одной строке; поместите курсор где-нибудь в теле лямбды и нажмите F9, и он остановит только лямбду. (Если вы сделаете это неправильно, он выделит всю строку цветом точки останова; если вы все сделаете правильно, он просто выделит лямбду.)

LINQ и IEnumerable<T> основано на тяге. Это означает, что предикаты и действия, которые являются частью оператора LINQ в целом, не выполняются, пока не будут получены значения. Кроме того, предикаты и действия будут выполняться каждый раз, когда извлекаются значения (например, не происходит никакого секретного кэширования).

Вытащить из IEnumerable<T> делается foreach утверждение, которое действительно является синтаксическим сахаром для получения перечислителя, вызвав IEnumerable<T>.GetEnumerator() и неоднократно звонил IEnumerator<T>.MoveNext() чтобы вытащить значения.

LINQ операторы, как ToList(), ToArray(), ToDictionary() а также ToLookup() оборачивает foreach заявление, так что эти методы будут тянуть. То же самое можно сказать о таких операторах, как Aggregate(), Count() а также First(), Общим для этих методов является то, что они дают один результат, который необходимо создать, выполнив foreach заявление.

Многие операторы LINQ производят новый IEnumerable<T> последовательность. Когда элемент извлекается из полученной последовательности, оператор извлекает один или несколько элементов из исходной последовательности. Select() оператор является наиболее очевидным примером, но другие примеры SelectMany(), Where(), Concat(), Union(), Distinct(), Skip() а также Take(), Эти операторы не делают никакого кэширования. Когда тогда N-й элемент извлекается из Select() он извлекает N-й элемент из исходной последовательности, применяет проекцию, используя предоставленное действие, и возвращает его. Ничего секретного здесь не происходит.

Другие операторы LINQ также производят новые IEnumerable<T> последовательности, но они реализуются путем фактического извлечения всей исходной последовательности, выполнения своей работы и последующего создания новой последовательности. Эти методы включают Reverse(), OrderBy() а также GroupBy(), Однако вытягивание, выполняемое оператором, выполняется только тогда, когда сам оператор вытягивается, что означает, что вам все еще нужно foreach цикл "в конце" оператора LINQ, прежде чем что-либо выполняется. Вы можете утверждать, что эти операторы используют кеш, потому что они сразу вытягивают всю исходную последовательность. Однако этот кеш создается каждый раз, когда оператор повторяется, так что это действительно деталь реализации, а не то, что волшебным образом обнаружит, что вы применяете то же самое OrderBy() операция несколько раз в той же последовательности.


В вашем примере ToList() будет тянуть Действие во внешнем Select выполнит 100 раз. Каждый раз, когда это действие выполняется Aggregate() сделает еще одну попытку, которая будет анализировать атрибуты XML. Всего ваш код позвонит Int32.Parse() 200 раз.

Вы можете улучшить это, потянув атрибуты один раз вместо каждой итерации:

var X = XElement.Parse (@"
    <ROOT>
        <MUL v='2' />
        <MUL v='3' />
    </ROOT>
")
.Elements ()
.Select (t => Int32.Parse (t.Attribute ("v").Value))
.ToList ();
Enumerable.Range (1, 100) 
    .Select (s => x.Aggregate (s, (t, u) => t * u)) 
    .ToList () 
    .ForEach (s => Console.WriteLine (s)); 

Сейчас Int32.Parse() вызывается только 2 раза. Однако стоимость заключается в том, что список значений атрибутов должен быть распределен, сохранен и, в конечном итоге, собран мусором. (Не большая проблема, когда список содержит два элемента.)

Обратите внимание, что если вы забудете первый ToList() который извлекает атрибуты, код будет по-прежнему выполняться, но с теми же характеристиками производительности, что и исходный код. Для хранения атрибутов не используется пространство, но они анализируются на каждой итерации.

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