Почему LINQ.Where(предикат).First() быстрее, чем .First(предикат)?

Я выполняю некоторые тесты производительности и заметил, что выражение LINQ, например

result = list.First(f => f.Id == i).Property

медленнее, чем

result = list.Where(f => f.Id == i).First().Property

Это кажется встречным интуитивным. Я бы подумал, что первое выражение будет быстрее, потому что оно может остановить итерирование по списку, как только предикат будет удовлетворен, тогда как я бы подумал, что выражение .Where() может перебирать весь список перед вызовом .First() on полученное подмножество. Даже если последнее делает короткое замыкание, оно не должно быть быстрее, чем использование First напрямую, но это так.

Ниже приведены два действительно простых модульных теста, которые иллюстрируют это. При компиляции с оптимизацией на TestWhereAndFirst примерно на 30% быстрее, чем TestFirstOnly на .Net и Silverlight 4. Я попытался сделать предикат возвращением большего количества результатов, но разница в производительности одинакова.

Может ли кто-нибудь объяснить, почему .First(fn) медленнее, чем .Where(fn).First()? Я вижу аналогичный счетчик с интуитивным результатом с .Count(fn) по сравнению с .Where(fn).Count().

private const int Range = 50000;

private class Simple
{
   public int Id { get; set; }
   public int Value { get; set; }
}

[TestMethod()]
public void TestFirstOnly()
{
   List<Simple> list = new List<Simple>(Range);
   for (int i = Range - 1; i >= 0; --i)
   {
      list.Add(new Simple { Id = i, Value = 10 });
   }

   int result = 0;
   for (int i = 0; i < Range; ++i)
   {
      result += list.First(f => f.Id == i).Value;
   }

   Assert.IsTrue(result > 0);
}

[TestMethod()]
public void TestWhereAndFirst()
{
   List<Simple> list = new List<Simple>(Range);
   for (int i = Range - 1; i >= 0; --i)
   {
      list.Add(new Simple { Id = i, Value = 10 });
   }

   int result = 0;
   for (int i = 0; i < Range; ++i)
   {
      result += list.Where(f => f.Id == i).First().Value;
   }

   Assert.IsTrue(result > 0);
}

Ответ 1

Я получил те же результаты: где + сначала быстрее, чем раньше.

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

Глядя в Reflector, сначала использует простой цикл foreach для итерации по коллекции, но там, где есть множество итераторов, специализированных для разных типов коллекций (массивы, списки и т.д.). Предположительно, это то, что дает Где небольшое преимущество.