Передает ли System.Linq.Enumerable.Reverse все элементы внутри массива?

Несколько лет назад кто-то жаловался на реализацию Linq.Reverse(), и Microsoft обещала исправить это. Это было в 2008 году, поэтому вопрос заключается в том, имеет ли Framework 4 оптимизированную реализацию Linq.Reverse(), которая не материализует сбор (т.е. копирует все элементы во внутренний массив), когда тип коллекции позволяет это (например, IList<T>)?

Ответ 1

Очевидно, что невозможно оптимизировать все случаи. Если какой-то объект реализует только IEnumerable<T>, а не IList<T>, вам нужно повторить его до конца, чтобы найти последний элемент. Таким образом, оптимизация была бы только для типов, которые реализуют IList<T> (например, T[] или List<T>).

Теперь, действительно ли он оптимизирован в .Net 4.5 DP? Разрешить запуск Отражатель ILSpy:

public static IEnumerable<TSource> Reverse<TSource>(
    this IEnumerable<TSource> source)
{
    if (source == null)
    {
        throw Error.ArgumentNull("source");
    }
    return ReverseIterator<TSource>(source);
}

Хорошо, как выглядит ReverseIterator<TSource>()?

private static IEnumerable<TSource> ReverseIterator<TSource>(
    IEnumerable<TSource> source)
{
    Buffer<TSource> buffer = new Buffer<TSource>(source);
    for (int i = buffer.count - 1; i >= 0; i--)
    {
        yield return buffer.items[i];
    }
    yield break;
}

То, что делает этот блок-итератор, - это создать Buffer<T> для коллекции и прокрутить назад. Мы почти там, что Buffer<T>?

[StructLayout(LayoutKind.Sequential)]
internal struct Buffer<TElement>
{
    internal TElement[] items;
    internal int count;
    internal Buffer(IEnumerable<TElement> source)
    {
        TElement[] array = null;
        int length = 0;
        ICollection<TElement> is2 = source as ICollection<TElement>;
        if (is2 != null)
        {
            length = is2.Count;
            if (length > 0)
            {
                array = new TElement[length];
                is2.CopyTo(array, 0);
            }
        }
        else
        {
            foreach (TElement local in source)
            {
                if (array == null)
                {
                    array = new TElement[4];
                }
                else if (array.Length == length)
                {
                    TElement[] destinationArray = new TElement[length * 2];
                    Array.Copy(array, 0, destinationArray, 0, length);
                    array = destinationArray;
                }
                array[length] = local;
                length++;
            }
        }
        this.items = array;
        this.count = length;
    }

    // one more member omitted
}

Что мы здесь? Мы копируем содержимое в массив. В каждом случае. Единственная оптимизация заключается в том, что если мы знаем Count (т.е. Коллекция реализует ICollection<T>), нам не нужно перераспределять массив.

Таким образом, оптимизация для IList<T> не в .Net 4.5 DP. Он создает копию всей коллекции в каждом случае.

Если бы я догадался, почему он не оптимизирован, прочитав статью Jon Skeet по этому вопросу, я думаю, это потому, что эта оптимизация наблюдаемым. Если вы мутируете коллекцию во время итерации, вы увидите измененные данные с оптимизацией, но старые данные без нее. И оптимизация, которая на самом деле изменяет поведение чего-то тонким способом, является плохой, из-за обратной совместимости.

Ответ 2

EDIT: Да, похоже, что это изменение было сделано

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

static void Main(string[] args)
{
    List<int> bigList = Enumerable.Range(0, 100000000).ToList();

    Console.WriteLine("List allocated");
    Console.ReadKey();

    foreach (int n in bigList.Reverse<int>())
    {
        // This will never be true, but the loop ensures that we enumerate
        // through the return value of Reverse()
        if (n > 100000000)
            Console.WriteLine("{0}", n);
    }
}

Идея состоит в том, что программа выделяет 400 МБ пространства в bigList, затем ждет, пока пользователь нажмет клавишу, а затем вызовет Enumerable.Reverse(bigList) через синтаксис метода расширения.

Я протестировал эту программу с помощью сборки Debug на компьютере с Windows 7 x64. По словам менеджера задач, использование памяти перед запуском программы составляет 2,00 ГБ. Затем, прежде чем я нажму ключ, использование памяти достигнет 2,63 ГБ. После того, как я нажал клавишу, использование памяти сократилось до 2,75 ГБ. Важно отметить, что это не шип 400 МБ или более, что было бы, если Enumerable.Reverse() делал копию.

ОРИГИНАЛЬНАЯ ПОЧТА

Невозможно для правильной реализации Enumerable.Reverse() не копировать в массив или другую структуру данных в некоторых ситуациях.

Жалоба, на которую вы ссылаетесь, касается только IList<T>. В общем случае я утверждаю, что Enumerable.Reverse() должен скопировать элементы в некоторый внутренний буфер.

Рассмотрим следующий метод

private int x = 0;

public IEnumerable<int> Foo()
{
    for (int n = 0; n < 1000; n++)
    {
        yield return n;
        x++;
    }
}

Теперь скажем, что Enumerable.Reverse() не копировал вход IEnumerable<T> в буфер в этом случае. Тогда петля

foreach (int n in Foo().Reverse())
    Console.WriteLine("{0}", n);

будет проходить весь процесс через блок итератора, чтобы получить первый n, полностью через первые 999 элементов, чтобы получить второй n и так далее. Но это не оказало бы такого же влияния на x, что и на итерацию вперед, потому что мы будем мутировать x каждый раз, когда мы повторяем почти весь путь через возвращаемое значение Foo(). Чтобы предотвратить это отключение между итерацией вперед и назад, метод Enumerable.Reverse() должен сделать копию ввода IEnumerable<T>.