Почему WhereSelectArrayIterator не реализует ICollection? - PullRequest
1 голос
/ 16 августа 2011

При просмотре System.Linq.Enumerable через Reflector я заметил, что итератор по умолчанию используется для Выберите и Где Методы расширения - WhereSelectArrayIterator - не реализует интерфейс ICollection . Если я правильно прочитал код, это вызывает некоторые другие методы расширения, такие как Count () и ToList () работают медленнее:

public static IEnumerable<TResult> Select<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, TResult> selector)
{
    // code above snipped
    if (source is List<TSource>)
    {
        return new WhereSelectListIterator<TSource, TResult>((List<TSource>) source, null, selector);
    }
    // code below snipped
}

private class WhereSelectListIterator<TSource, TResult> : Enumerable.Iterator<TResult>
{
    // Fields
    private List<TSource> source; // class has access to List source so can implement ICollection
    // code below snipped
}


public class List<T> : IList<T>, ICollection<T>, IEnumerable<T>, IList, ICollection, IEnumerable
{
public List(IEnumerable<T> collection)
{
    ICollection<T> is2 = collection as ICollection<T>;
    if (is2 != null)
    {
        int count = is2.Count;
        this._items = new T[count];
        is2.CopyTo(this._items, 0); // FAST
        this._size = count;
    }
    else
    {
        this._size = 0;
        this._items = new T[4];
        using (IEnumerator<T> enumerator = collection.GetEnumerator())
        {
            while (enumerator.MoveNext())
            {
                this.Add(enumerator.Current);  // SLOW, CAUSES ARRAY EXPANSION
            }
        }
    }
}

}

Я проверил это с результатами, подтверждающими мое подозрение:

ICollection: 2388,5222 мс

IEnumerable: 3308,3382 мс

Вот тестовый код:

    // prepare source
    var n = 10000;
    var source = new List<int>(n);
    for (int i = 0; i < n; i++) source.Add(i);

    // Test List creation using ICollection
    var startTime = DateTime.Now;
    for (int i = 0; i < n; i++)
    {
        foreach(int l in source.Select(k => k)); // itterate to make comparison fair
        new List<int>(source);
    }
    var finishTime = DateTime.Now;
    Response.Write("ICollection: " + (finishTime - startTime).TotalMilliseconds + " ms <br />");

    // Test List creation using IEnumerable
    startTime = DateTime.Now;
    for (int i = 0; i < n; i++) new List<int>(source.Select(k => k));
    finishTime = DateTime.Now;
    Response.Write("IEnumerable: " + (finishTime - startTime).TotalMilliseconds + " ms");

Я что-то упустил или это будет исправлено в будущих версиях Framework?

Спасибо за ваши мысли.

1 Ответ

5 голосов
/ 17 августа 2011

LINQ to Objects использует некоторые приемы для оптимизации определенных операций. Например, если вы объедините два оператора .Where вместе, предикаты будут объединены в один WhereArrayIterator, поэтому предыдущие могут быть собраны сборщиком мусора. Аналогично, Where, за которым следует Select, создаст WhereSelectArrayIterator, передавая объединенные предикаты в качестве аргумента, так что исходный WhereArrayiterator можно будет собирать мусором. Таким образом, WhereSelectArrayIterator отвечает за отслеживание не только selector, но также и объединенного predicate, на котором оно может основываться или не основываться.

Поле source отслеживает только начальный список, который был задан. Из-за предиката результат итерации не всегда будет иметь такое же количество элементов, как source. Поскольку LINQ предназначен для ленивой оценки, он не должен заранее сравнивать source с predicate, чтобы потенциально сэкономить время, если кто-нибудь в итоге вызовет .Count(). Это может вызвать такой же удар по производительности, как и вызов .ToList() для него вручную, и если пользователь пропустит его через несколько предложений Where и Select, вы в конечном итоге создадите несколько списков без необходимости.

Может ли LINQ to Object быть реорганизован для создания SelectArrayIterator, который он использует при вызове Select непосредственно в массиве? Конечно. Будет ли это повысить производительность? Совсем немного. По какой цене? Меньшее повторное использование кода означает дополнительный код для поддержки и тестирования продвижения вперед.

И, таким образом, мы добрались до сути подавляющего большинства вопросов «Почему у языка / платформы X нет функции Y»: с ​​каждой функцией и оптимизацией связана некоторая стоимость, и даже у Microsoft нет неограниченных ресурсов , Как и любая другая компания, они делают оценочные вызовы, чтобы определить, как часто будет выполняться код, который выполняет Select для массива, а затем вызывает .ToList() для него, и стоит ли писать этот процесс немного быстрее и ведение другого класса в пакете LINQ.

...