Который быстрее: Single (предикат) или Where (предикат).Single()

Обсуждение в результате этого ответа мне любопытно. Что быстрее:

someEnumerable.Single(predicate);

или

someEnumerable.Where(predicate).Single();

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

Даже ReSharper предлагает первое:

enter image description here

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

Ответ 1

LINQ к объектам

Ничто не отвечает на такой вопрос, как эталон:

(обновлено)

class Program
{
    const int N = 10000;
    volatile private static int s_val;

    static void DoTest(IEnumerable<int> data, int[] selectors) {
        Stopwatch s;

        // Using .Single(predicate)
        s = Stopwatch.StartNew();
        foreach (var t in selectors) {
            s_val = data.Single(x => x == t);
        }
        s.Stop();
        Console.WriteLine("   {0} calls to Single(predicate) took {1} ms.",
            selectors.Length, s.ElapsedMilliseconds);

        // Using .Where(predicate).Single()
        s = Stopwatch.StartNew();
        foreach (int t in selectors) {
            s_val = data.Where(x => x == t).Single();
        }
        s.Stop();
        Console.WriteLine("   {0} calls to Where(predicate).Single() took {1} ms.",
            selectors.Length, s.ElapsedMilliseconds);
    }


    public static void Main(string[] args) {
        var R = new Random();
        var selectors = Enumerable.Range(0, N).Select(_ => R.Next(0, N)).ToArray();

        Console.WriteLine("Using IEnumerable<int>  (Enumerable.Range())");
        DoTest(Enumerable.Range(0, 10 * N), selectors);

        Console.WriteLine("Using int[]");
        DoTest(Enumerable.Range(0, 10*N).ToArray(), selectors);

        Console.WriteLine("Using List<int>");
        DoTest(Enumerable.Range(0, 10 * N).ToList(), selectors);

        Console.ReadKey();
    }
}

Несколько шокирует, .Where(predicate).Single() выигрывает примерно в два раза. Я даже дважды запускал оба случая, чтобы убедиться, что кеширование и т.д. Не было фактором.

1) 10000 calls to Single(predicate) took 7938 ms.
1) 10000 calls to Where(predicate).Single() took 3795 ms.
2) 10000 calls to Single(predicate) took 8132 ms.
2) 10000 calls to Where(predicate).Single() took 4318 ms.

Обновленные результаты:

Using IEnumerable<int>  (Enumerable.Range())
   10000 calls to Single(predicate) took 7838 ms.
   10000 calls to Where(predicate).Single() took 8104 ms.
Using int[]
   10000 calls to Single(predicate) took 8859 ms.
   10000 calls to Where(predicate).Single() took 2970 ms.
Using List<int>
   10000 calls to Single(predicate) took 9523 ms.
   10000 calls to Where(predicate).Single() took 3781 ms.

Ответ 2

Where(predicate).Single() будет быстрее, чем Singe(predicate)

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

Дополнительная достопримечательность (оригинальный ответ) - Where выполняет специальные оптимизации для разных типов типов коллекций, тогда как другие методы, такие как First, Single и Count, не используют тип коллекции.

Итак, Where(predicate).Single() может выполнять некоторые оптимизации, которые Single(predicate) не

Ответ 3

Основываясь на фактической реализации для Where(predicate).Single() и Single(predicate), кажется, что первая на самом деле ленива, тогда как последняя всегда выполняет итерацию по всему IEnumerable. Single() оба возвращают единственный элемент перечисления, но также проверяют, не имеет ли перечисление ни одного или имеет более одного значения, чего можно добиться просто, просто задав следующие 2 элемента перечисления. Single(predicate) в настоящее время реализуется таким образом, что ему нужно перебирать по всей нумерации, чтобы подтвердить, является ли предикат true для одного и только одного элемента, таким образом, разница в производительности (и функциональная, см. ниже).

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

public IEnumerable<int> InfiniteEnumeration()
{
    while (true)
    {
        yield return 1;
    }
}

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

var singleUsingWhere = InfiniteEnumeration().Where(value => value != 0).Single();
var singleUsingSingle = InfiniteEnumeration().Single(value => value != 0);

Странно, что Microsoft решила реализовать Single(predicate) таким образом... даже Jon Skeet удалось зафиксировать этот недосмотр.

Ответ 4

В объекте Linq-for-objects Single имеется конструктивный недостаток, что означает:

  • Бессмысленно держать подсчет количества совпадений, а не находить совпадение, а затем бросать, если находит другое.
  • Он продолжается, пока не достигнет конца последовательности, даже после третьего матча.
  • Он может бросать OverflowException; это вряд ли, но что это вообще может быть ошибкой.

https://connect.microsoft.com/VisualStudio/feedback/details/810457/public-static-tsource-single-tsource-this-ienumerable-tsource-source-func-tsource-bool-predicate-doesnt-throw-immediately-on-second-matching-result#

Это делает его немного медленнее в случае совпадений 0 или 1 (конечно, только второй случай не является ошибкой) и намного медленнее в случае более чем одного совпадения (случай ошибки).

С другими поставщиками Linq это зависит; он имеет тенденцию быть примерно одинаковым, но вполне возможно, чтобы данный провайдер был менее эффективным с тем или иным, а другой данный провайдер был обратным.

[Изменить: это больше не относится к .NET Core, в котором вышеприведенное описание больше не применяется. Это делает один вызов .Single(pred) очень немного более эффективным, чем .Where(pred).Single().]

Ответ 5

Я думаю, что это случай яблок против апельсинов.

Нам нужно подумать, как текущая реализация Single(predicate) отличается от следующей реализации:

public static TSource Single<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate)
{
    return Where(source, predicate).Single();
}

Реализация Where возвращает Enumerable.Iterator, которая выглядит так, как будто она распознает условие гонки, которое возникает, когда MoveNext вызывается на том же итераторе в разных потоках.

От ILSpy:

switch (this.state)
{
case 1:
    this.enumerator = this.source.GetEnumerator();
    this.state = 2;
    break;
case 2:
    break;
default:
    return false;
}

Текущая реализация Single(predicate) и First(predicate) не обрабатывает это условие.

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