Методы расширения в IEnumerable: какова его производительность?

#.net #linq #performance

#.net #linq #Производительность

Вопрос:

От моего наставника: предпочитайте собственные методы (реализованные непосредственно в коллекции) методам расширения IEnumerable, потому что:

Методы расширения LINQ-to-Objects реализованы в IEnumerable, что означает, что в наихудшем сценарии (когда элемент, который вы ищете, не существует в коллекции) вам придется перечислять все элементы. Если у вас есть метод Contains или Exists, реализованный непосредственно в коллекции, он может использовать внутренние знания и, возможно, просто выполнить поиск по хэш-таблице или какую-либо другую быструю операцию.

Я был в глубоком замешательстве, потому что я думаю, что Microsoft должна была реализовать хэш-таблицу для IEnumerable, которая уже содержит / существует. Быстрый тест с List и IEnumerable не показывает различий:

 static void Main(string[] args)
{
    Console.Write("input the number of elements: ");
    int count = Convert.ToInt32(Console.ReadLine());
    Console.Write("input the number of loops: ");
    int loop = Convert.ToInt32(Console.ReadLine());

    Random r = new Random();

    Stopwatch sw = new Stopwatch();
    for (int i = 0; i < loop; i  )
    {
        var list = CreateListOfInt(count);
        sw.Start();
        for (int j = 0; j < count; j  )
        {
            DoContains(list, r.Next());
        }
        sw.Stop();
    }

    Console.WriteLine("List<T> native method: Iterated {0} times on {1} elements, elapsed :{2}",loop,count,sw.Elapsed);

    sw.Reset();
    for (int i = 0; i < loop; i  )
    {
        var list = CreateListOfInt(count);
        sw.Start();
        for (int j = 0; j < count; j  )
        {
            DoContainsEnumerable(list, r.Next());
        }
        sw.Stop();
    }

    Console.WriteLine("IEnumerable<T> extension method: Iterated {0} times on {1} elements, elapsed :{2}", loop, count, sw.Elapsed);

    sw.Reset();
    for (int i = 0; i < loop; i  )
    {
        var list = CreateListOfInt2(count);
        sw.Start();
        for (int j = 0; j < count; j  )
        {
            //make sure that the element is not in the list
            DoContains(list, r.Next(20000, 50000));
        }
        sw.Stop();
    }
    Console.WriteLine("List<T> native method: element does not exist:Iterated {0} times on {1} elements, elapsed :{2}", loop, count, sw.Elapsed);

    sw.Reset();
    for (int i = 0; i < loop; i  )
    {
        var list = CreateListOfInt2(count);
        sw.Start();
        for (int j = 0; j < count; j  )
        {
            //make sure that the element is not in the list
            DoContainsEnumerable(list, r.Next(20000, 50000));
        }
        sw.Stop();
    }
    Console.WriteLine("IEnumerable<T> extension method: element does not exist: Iterated {0} times on {1} elements, elapsed :{2}", loop, count, sw.Elapsed);


    Console.ReadKey();
}

static List<int> CreateListOfInt(int count)
{
    Random r = new Random(1000);
    List<int> numbers = new List<int>(count);
    for (int i = 0; i < count; i  )
    {
        numbers.Add(r.Next());
    }
    return numbers;
}

static bool DoContains(List<int> list, int number)
{
    return list.Contains(number);
}

static bool DoContainsEnumerable(IEnumerable<int> list, int number)
{
    return list.Contains(number);
}


//define the scope of randomly created number, to make sure that lookup number will not in the List
static List<int> CreateListOfInt2(int count)
{
    Random r = new Random(1000);
    List<int> numbers = new List<int>(count);
    for (int i = 0; i < count; i  )
    {
        numbers.Add(r.Next(0,10000));
    }
    return numbers;
}
  

}

Редактировать: я попробовал реализацию HashSet, которая значительно повышает производительность:

   sw.Reset();
            for (int i = 0; i < loop; i  )
            {
                var list = CreateListOfInt2(count);
                HashSet<int> hashtable = new HashSet<int>(list);
                sw.Start();
                for (int j = 0; j < count; j  )
                {
                    //make sure that the element is not in the list
                    hashtable.Contains(r.Next(20000, 50000));
                }
                sw.Stop();
            }
            Console.WriteLine("IEnumerable<T> extension method: element does not exist: Iterated {0} times on {1} elements, elapsed :{2}", loop, count, sw.Elapsed);
  

И все же, что вы думаете о словах моего наставника?

Кто-нибудь может прояснить для меня? Прав ли мой наставник? Если он прав, что не так с моим кодом?

Большое вам спасибо

Ответ №1:

List<T> Contains вызовы просто повторяют список, поэтому они не будут быстрее, чем метод расширения. Если бы вы использовали HashSet<T> и попробовали серию Contains() операций, вы бы обнаружили заметное улучшение.

Редактировать: причина, по которой Microsoft не использовала хэш для IEnumerable<T> методов расширения, заключается в том, что они не могли гарантировать, что реализующий класс использовал хэш или что-то подобное. Им пришлось использовать наивный подход, потому что IEnumerable<T> интерфейс гарантирует только перечисление реализующего класса.

Комментарии:

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

2. Перечисляемый. Содержит uses ICollection. Содержит, когда это возможно. Поскольку HashSet реализует ICollection, который содержит собственный, он будет использоваться LINQ, поэтому нет разницы между LINQ и Native.

3. можете ли вы привести пример того, как HashSet улучшает производительность Contains?

4. @Vimvq1987: в поиске по хэш-набору сложность равна O (1), а в списке O (N).

Ответ №2:

Если версия LINQ имеет более быструю встроенную реализацию для объекта, то вместо этого используется эта более быстрая реализация.

Например, Count реализовано следующим образом:

 if (source is Array)
    return source.Length;
if (source is ICollection)
    return source.Count;
// else iterate through all the items and count them.
  

Contains вот так:

 if (source is ICollection)
    return source.Contains(item);
// else iterate through the enumerable, and see if item exists
  

Поскольку используется HashSet<T> implements ICollection<T> , используется собственный Contains.

Итак, LINQ был оптимизирован для стандартных интерфейсов. Однако, если у вас есть пользовательский тип, который имеет собственный вызов, который не является частью интерфейса по умолчанию, тогда вызов LINQ может быть медленнее.