С#: производительность ToArray

История:

Я признаю, что не пытался сравнивать это, но мне любопытно...

Каковы характеристики CPU/памяти для Enumerable.ToArray<T> (и его кузена Enumerable.ToList<T>)?

Так как IEnumerable не рекламирует заранее, сколько элементов он имеет, я (возможно наивно) предположим, что ToArray должен "угадать" начальный размер массива, а затем изменить размер/перераспределить массив, если первое предположение кажется слишком маленьким, а затем для его изменения еще раз, если второе предположение кажется слишком маленьким и т.д.... Это может привести к ухудшению качества работы.

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

Вопрос:

Существует ли какая-то "магия" за кулисами, которая позволяет избежать необходимости повторного изменения размера и делает ToArray линейной по пространству и времени?

В целом, существует ли "официальная" документация по характеристикам производительности BCL?

Ответ 1

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

Обратите внимание, что это не всегда требуется. Если IEnumerable<T> является .ToArray ed также реализует ICollection<T>, то свойство .Count используется для предварительного выделения массива (что делает алгоритм линейным по пространству и времени). Если нет, то, однако, следующее (грубое ):

    foreach (TElement current in source)
    {
        if (array == null)
        {
            array = new TElement[4];
        }
        else
        {
            if (array.Length == num)
            {
                // Doubling happens *here*
                TElement[] array2 = new TElement[checked(num * 2)];
                Array.Copy(array, 0, array2, 0, num);
                array = array2;
            }
        }
        array[num] = current;
        num++;
    }

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

Несмотря на это, обычно рекомендуется избегать вызова .ToArray() и .ToList(), если вы его не требуете. Опрос запроса непосредственно при необходимости часто является лучшим выбором.

Ответ 2

Я извлек код позади метода .ToArray(), используя .NET Reflector:

public static TSource[] ToArray<TSource>(this IEnumerable<TSource> source)
{
    if (source == null)
    {
        throw Error.ArgumentNull("source");
    }
    Buffer<TSource> buffer = new Buffer<TSource>(source);
    return buffer.ToArray();
}

и Buffer.ToArray:

internal TElement[] ToArray()
{
    if (this.count == 0)
    {
        return new TElement[0];
    }
    if (this.items.Length == this.count)
    {
        return this.items;
    }
    TElement[] destinationArray = new TElement[this.count];
    Array.Copy(this.items, 0, destinationArray, 0, this.count);
    return destinationArray;
}

И внутри конструктора Buffer он проходит через все элементы для вычисления реального Count и массива элементов.

Ответ 3

IIRC, он использует алгоритм удвоения.

Помните, что для большинства типов все, что вам нужно сохранить, это ссылки. Не похоже, что вы выделяете достаточно памяти для копирования всего объекта (если, конечно, вы не используете много структур... tsk tsk).

По-прежнему рекомендуется избегать использования .ToArray() или .ToList() до последнего момента. В большинстве случаев вы можете просто продолжать использовать IEnumerable <T> до тех пор, пока вы не запустите цикл foreach или не назначите его источнику данных.