В котором случаи являются IEnumerable <T>.Count оптимизированный?

Используя отражатель я заметил это System.Linq.Enumerable.Count метод имеет условие в нем для оптимизации его для случая когда IEnumerable переданный на самом деле ICollection. Если бросок успешно выполняется, Метод счета не должен выполнять итерации по каждому элементу, но может назвать Метод счета ICollection.

На основе этого я начинал думать это IEnumerable может использоваться как представление только для чтения набора, не имея потери производительности, из которой я первоначально ожидал на основе API IEnumerable

Мне было интересно ли оптимизация Count все еще содержит когда IEnumerable результат a Select оператор по ICollection, но на основе отраженного кода этот случай не оптимизирован и требует повторения через все элементы.

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

14
задан sth 2 February 2010 в 09:31
поделиться

3 ответа

Не имеет значения, что результат Select оценивается лениво. Результат Count всегда эквивалентен счету исходной коллекции, поэтому его можно было бы получить непосредственно, вернув конкретный объект из Select, который можно было бы использовать для оценки короткого замыкания в методе Count.

Причина, по которой невозможно оптимизировать оценку метода Count() на возвращаемом значении вызова Select из чего-либо с определенным количеством (например, List), заключается в том, что это может изменить смысл программы.

Функция селектора , переданная в метод Select, позволяет иметь побочные эффекты , а его побочные эффекты должны происходить детерминированно, в предопределённом порядке.

Предположим:

new[]{1,2,3}.Select(i => { Console.WriteLine(i); return 0; }).Count();

Документация требует, чтобы этот код был напечатан

1
. 2
3

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


UPDATE: Очевидно, не совсем понятно, что вполне возможно реализовать Select и Count так, чтобы Selects на ICollection все равно лениво вычислялись, но Count() будет вычисляться в O(1) без перебора коллекции. Я сделаю это без изменения интерфейса каких-либо методов. Аналогичное уже сделано для ICollection:

private interface IDirectlyCountable {
    int Count {get;}
}
private class SelectICollectionIterator<TSource,TResult> : IEnumerable<T>, IDirectlyCountable {
     ICollection<TSource> sequence;
     Func<TSource,TResult> selector;
     public SelectICollectionIterator(ICollection<TSource> source, Func<TSource,TResult> selector) {
         this.sequence = source;
         this.selector = selector;
     }
     public int Count { get { return sequence.Count; } }
     // ... GetEnumerator ... 
}
public static IEnumerable<TResult> Select<TSource,TResult>(this IEnumerable<TSource> source, Func<TSource,TResult> selector) {
    // ... error handling omitted for brevity ...
    if (source is ICollection<TSource>)
       return new SelectICollectionIterator<TSource,TResult>((ICollection<TSource>)source, selector);
    // ... rest of the method ...
}
public static int Count<T>(this IEnumerable<T> source) {
    // ...
    ICollection<T> collection = source as ICollection<T>;
    if (collection != null) return collection.Count;
    IDirectlyCountable countableSequence = source as IDirectlyCountable;
    if (countableSequence != null) return countableSequence.Count;
    // ... enumerate and count the sequence ...
}

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

12
ответ дан 1 December 2019 в 14:32
поделиться

AN ICOLLECTION знает количество элементов (подсчет), которое он содержит. Это не должно повторять какие-либо предметы, чтобы определить его. Возьмите, например, класс класса (который реализует ychollce ).

AN IENumerable не знает, сколько элементов он содержит. Вы должны перечислить весь список, чтобы определить количество элементов (счетчика).

Упаковка Щелк в операторе LINQ не делает его более эффективным. Независимо от того, как вы поворачиваете и поворачиваются, амоклек должен быть перечислен.

0
ответ дан 1 December 2019 в 14:32
поделиться

Edit 02-Feb-2010 :

На мой взгляд, есть как минимум два способа интерпретировать этот вопрос.

Почему метод расширения Select , когда вызвал экземпляр класса, который реализует ICollection , а не возвращать объект, который предоставляет свойство Count ; и почему метод расширения Count не проверяет это свойство, чтобы обеспечить производительность O (1), когда два {{ 1}} связаны цепочкой?

Эта версия вопроса не делает ложных предположений о том, как работают расширения Linq, и является правильным вопросом, поскольку вызов ICollection .Select.Count будет, в конце концов, всегда возвращайте то же значение, что и ICollection .Count . Вот как Мехрдад истолковал вопрос, на который он дал обстоятельный ответ.

Но Я прочитал вопрос как вопрос ...

Если метод расширения Count обеспечивает производительность O (1) для объекта класса , реализующего ICollection , почему обеспечивает производительность O (n) для возвращаемого значения Select метод расширения?

В этой версии вопроса существует ошибочное предположение: методы расширения Linq работают вместе, собирая небольшие коллекции одна за другой (в памяти) и выставляя их через интерфейс IEnumerable .

Если бы так работали расширения Linq, метод Select мог бы выглядеть примерно так:

public static IEnumerable<TResult> Select<T, TResult>(this IEnumerable<T> source, Func<T, TResult> selector) {
    List<TResult> results = new List<TResult>();

    foreach (T input in source)
        results.Add(selector(input));

    return results;
}

Более того, если бы этот был реализацией Select ], Я думаю, вы обнаружите, что большая часть кода, использующего этот метод, будет вести себя точно так же. Но это было бы расточительно и фактически привело бы к исключениям в некоторых случаях, подобных тому, который я описал в моем первоначальном ответе.

На самом деле, я считаю, что реализация метода Select намного ближе к чему-то вроде этого:

public static IEnumerable<TResult> Select<T, TResult>(this IEnumerable<T> source, Func<T, TResult> selector) {
    foreach (T input in source)
        yield return selector(input);

    yield break;
}

Это обеспечивает ленивую оценку и объясняет, почему свойство Count недоступен за время O (1) для метода Count .

Другими словами, в то время как Мехрдад ответил на вопрос , почему Select не был разработан иначе, чтобы Select.Count вел себя иначе , я предложил свой лучший ответ на вопрос , почему Select.Count ведет себя так же, как .


ОРИГИНАЛЬНЫЙ ОТВЕТ :

Побочные эффекты метода - это не ответ.

Согласно ответу Мехрдада:

На самом деле не имеет значения, что результат операции Select вычисляется лениво.

Я не куплюсь на это. Позвольте мне объяснить почему.

Для начала рассмотрим два очень похожих метода:

public static IEnumerable<double> GetRandomsAsEnumerable(int N) {
    Random r = new Random();

    for (int i = 0; i < N; ++i)
        yield return r.NextDouble();

    yield break;
}

public static double[] GetRandomsAsArray(int N) {
    Random r = new Random();

    double[] values = new double[N];
    for (int i = 0; i < N; ++i)
        values[i] = r.NextDouble();

    return values;
}

Хорошо, что делают эти методы? Каждый из них возвращает столько случайных удвоений, сколько желает пользователь (до int.MaxValue ).Имеет ли значение, лениво оценивается тот или иной метод? Чтобы ответить на этот вопрос, давайте взглянем на следующий код:

public static double Invert(double value) {
    return 1.0 / value;
}

public static void Test() {
    int a = GetRandomsAsEnumerable(int.MaxValue).Select(Invert).Count();
    int b = GetRandomsAsArray(int.MaxValue).Select(Invert).Count();
}

Можете ли вы угадать, что произойдет с этими двумя вызовами методов? Позвольте мне избавить вас от необходимости скопировать этот код и протестировать его самостоятельно:

Первая переменная , a , будет (после потенциально значительного количества времени) инициализирована как int.MaxValue (в настоящее время 2147483647). второй , b , скорее всего, будет прерван из-за исключения OutOfMemoryException .

Поскольку Select и другие методы расширения Linq вычисляются лениво, они позволяют вам делать то, что вы просто не смогли бы сделать иначе. Выше приведен довольно тривиальный пример. Но моя главная цель - оспорить утверждение о том, что ленивое вычисление не важно. Утверждение Мердада о том, что свойство Count «действительно известно с самого начала и может быть оптимизировано», на самом деле вызывает вопрос. Проблема может показаться простой для метода Select , но Select на самом деле не является чем-то особенным; он возвращает IEnumerable , как и остальные методы расширения Linq, и чтобы эти методы «знали» Count их возвращаемых значений , потребуются полные коллекции кэшироваться и, следовательно, запрещать ленивую оценку .

Ответ - ленивое вычисление.

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

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

1
ответ дан 1 December 2019 в 14:32
поделиться
Другие вопросы по тегам:

Похожие вопросы: