Лучше ли вызывать ToList() или ToArray () в запросах LINQ?

Я часто сталкиваюсь с случаем, когда я хочу оценить запрос прямо там, где я его объявляю. Это обычно потому, что мне нужно повторить его несколько раз и это дорого вычислить. Например:

string raw = "...";
var lines = (from l in raw.Split('n')
             let ll = l.Trim()
             where !string.IsNullOrEmpty(ll)
             select ll).ToList();

это прекрасно работает. но если я не собираюсь изменять результат, то я мог бы также вызвать ToArray() вместо ToList().

мне интересно, однако, реализован ли ToArray() первым вызовом ToList () и, следовательно, меньше память эффективна, чем просто вызов ToList ().

Я сошел с ума? Должен ли я просто позвонить ToArray() - безопасно и безопасно, зная, что память не будет выделена дважды?

15 ответов


Если вам просто не нужен массив для удовлетворения других ограничений, вы должны использовать ToList. В большинстве сценариев ToArray выделяет больше памяти, чем ToList.

как использовать массивы для хранения, но ToList имеет более гибкое ограничение. Для этого массив должен быть по крайней мере таким же большим, как количество элементов в коллекции. Если массив больше, это не проблема. Однако ToArray необходимо, чтобы размер массива был точно равен числу элементы.

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

редактировать

A несколько человек спросили меня о последствиях наличия дополнительной неиспользуемой памяти в List<T> значение.

это веский аргумент. Если созданная коллекция долговечна, никогда не изменяется после создания и имеет высокую вероятность посадки в кучу Gen2, вам может быть лучше взять дополнительное выделение ToArray вперед.

в общем, хотя я считаю, что это более редкий случай. Гораздо чаще можно увидеть много ToArray вызовов немедленно передаются другим недолговечным видам использования памяти в этом случае ToList явно лучше.

ключ здесь-профиль, профиль, а затем профиль еще.


разница в производительности будет незначительной, так как List<T> реализован как массив динамического размера. Вызова ToArray() (который использует внутренний Buffer<T> класс для увеличения массива) или ToList() (который называет List<T>(IEnumerable<T>) конструктор) в конечном итоге будет заключаться в том, чтобы поместить их в массив и увеличить массив, пока он не подойдет им всем.

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


(семь лет спустя...)

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

этот пост является просто дополнением, чтобы упомянуть семантическая разница между IEnumerator<T> произведен массив (T[]) по сравнению с тем, что возвращается List<T>.

лучше всего иллюстрируется примером:

IList<int> source = Enumerable.Range(1, 10).ToArray();  // try changing to .ToList()

foreach (var x in source)
{
  if (x == 5)
    source[8] *= 100;
  Console.WriteLine(x);
}

приведенный выше код будет работать без исключения и производит вывод:

1
2
3
4
5
6
7
8
900
10

это показывает, что IEnumarator<int> возвращены int[] не отслеживает, был ли массив изменен с момента создания перечислителя.

обратите внимание, что я объявил локальную переменную source как IList<int>. Таким образом, я убеждаюсь, что компилятор C# не оптимизирует foreach утверждение во что-то, что эквивалентно for (var idx = 0; idx < source.Length; idx++) { /* ... */ } петли. Это то, что компилятор C# может сделать, если я использую var source = ...; вместо. В моей текущей версии .NET framework фактический перечислитель, используемый здесь, является непубличным ссылочным типом System.SZArrayHelper+SZGenericArrayEnumerator`1[System.Int32] но, конечно, это деталь реализации.

теперь, если я изменю .ToArray() на .ToList(), я получаю только:

1
2
3
4
5

затем System.InvalidOperationException взрыв, говоря:

коллекция была изменена; операция перечисления может не выполняться.

базовый перечислитель в этот случай является общедоступным изменяемым значением-type System.Collections.Generic.List`1+Enumerator[System.Int32] (в коробке внутри IEnumerator<int> box в этом случае, потому что я использую IList<int>).

в заключение перечислитель, произведенного List<T> отслеживает, изменяется ли список во время перечисления, в то время как перечислитель производится T[] нет. Поэтому учитывайте эту разницу при выборе между .ToList() и .ToArray().

люди часто добавляют один дополнительно .ToArray() или .ToList() чтобы обойти коллекцию, которая отслеживает, была ли она изменена в течение срока службы перечислителя.

(если кто-нибудь хочет знать как the List<> отслеживает, была ли изменена коллекция, есть частное поле _version в этом классе, который изменяется каждый раз, когда List<> обновляется.)


Я согласен с @mquander, что разница в производительности должна быть незначительной. Тем не менее, я хотел проверить его, чтобы быть уверенным, поэтому я сделал - и это, незначительно.

Testing with List<T> source:
ToArray time: 1934 ms (0.01934 ms/call), memory used: 4021 bytes/array
ToList  time: 1902 ms (0.01902 ms/call), memory used: 4045 bytes/List

Testing with array source:
ToArray time: 1957 ms (0.01957 ms/call), memory used: 4021 bytes/array
ToList  time: 2022 ms (0.02022 ms/call), memory used: 4045 bytes/List

каждый исходный массив/список 1000 элементов. Таким образом, вы можете видеть, что различия во времени и памяти незначительны.

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


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

список использует массив в качестве внутреннего хранилища и при необходимости удваивает емкость. Это означает, что в среднем 2/3 предметов были перераспределены по крайней мере один раз, половина из них перераспределена по крайней мере дважды, половина из них по крайней мере трижды, и так далее. Это означает, что каждый элемент был перераспределен в среднем 1,3 раза, что не очень накладно.

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


ToList() обычно предпочтительнее, если вы используете его на IEnumerable<T> (из ORM, например). Если длина последовательности не известна в начале,ToArray() создает коллекцию динамической длины, такую как List, а затем преобразует ее в array, что занимает дополнительное время.


редактировать: последняя часть этого ответа недействительна. Однако остальная информация по-прежнему полезна, поэтому я оставлю ее.

я знаю, что это старый пост, но после того, как тот же вопрос и некоторые исследования, я нашел что-то интересное, что может быть стоит поделиться.

во-первых, я согласен с @mquander и его ответом. Он прав, говоря, что с точки зрения производительности они идентичны.

, я использую Рефлектор, чтобы взглянуть на методы в System.Linq.Enumerable extensions namespace, и я заметил очень распространенную оптимизацию.
Когда это возможно,IEnumerable<T> источник приведен к IList<T> или ICollection<T> для оптимизации метода. Например, посмотрите на ElementAt(int).

интересно, что Microsoft решила оптимизировать только для IList<T>, а не IList. Похоже, Microsoft предпочитает использовать IList<T> интерфейс.

System.Array реализует только IList, так что не будет извлекать выгоду из любой из этих оптимизаций расширения.
Поэтому я утверждаю, что наилучшей практикой является использование .ToList() метод.
Если вы используете какой-либо из методов расширения или передаете список другому методу, есть вероятность, что он может быть оптимизирован для IList<T>.


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

если производительность имеет значение, вы должны также рассмотреть, что было бы быстрее оперировать. Реально, вы не позвоните ToList или ToArray миллион раз, но может работа над полученной коллекцией миллион раз. В этом отношении [] лучше, поскольку List<> is [] С некоторыми издержками. См. этот поток для сравнения эффективности:какой из них более эффективен : List или int[]

в моих тестах некоторое время назад, я нашел ToArray быстрее. И я не уверен, насколько искаженными были тесты. Этот разница в производительности настолько незначительна, что может быть заметна только при выполнении этих запросов в цикле миллионы раз.


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

они оба сосут, когда они создали с помощью LINQ. Они оба реализуют один и тот же код для при необходимости измените размер буфера. ToArray внутренне использует класс для преобразования IEnumerable<> для массива, выделив массив из 4 элементов. Если этого недостаточно, он удваивает размер, создавая новый массив, удваивая размер текущего и копируя текущий массив в него. В конце он выделяет новый массив count of your предметы. Если ваш запрос возвращает 129 элементов, то ToArray сделает 6 выделений и операций копирования памяти для создания массива 256 элементов, а затем другой массив 129 для возврата. так много для эффективности памяти.

ToList делает то же самое, но пропускает последнее распределение, так как вы можете добавлять элементы в будущем. Список не заботится о том, создан ли он из запроса linq или создан вручную.

для списка создания лучше с памятью, но хуже с cpu, так как список универсальное решение каждое действие требует проверки диапазона дополнительно к проверке внутреннего диапазона .net для массивов.

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

распределение инициализации списка может быть лучше, если вы укажете параметр емкости при его создании. В этом случае он будет выделять массив только один раз, если вы знать размер результата. ToList linq не указывает перегрузку для ее предоставления, поэтому мы должны создать наш метод расширения, который создает список с заданной емкостью, а затем использует List<>.AddRange.

чтобы закончить этот ответ, я должен написать следующие предложения

  1. в конце вы можете использовать либо ToArray, либо ToList, производительность не будет настолько отличаться (см. ответ @EMP ).
  2. вы используете C#. Если вам нужна производительность, не беспокойтесь о написании о высокопроизводительном коде, но беспокоиться о том, чтобы не писать плохой код производительности.
  3. всегда нацеливайте x64 на высокопроизводительный код. AFAIK, x64 JIT основан на компиляторе C++ и делает некоторые забавные вещи, такие как оптимизация хвостовой рекурсии.
  4. С 4.5 вы также можете наслаждаться оптимизацией профиля и многоядерным JIT.
  5. наконец, вы можете использовать шаблон async/await для его быстрой обработки.

Это старый вопрос-но в интересах пользователей, которые натыкаются на него, есть также и альтернатива "Memoizing" перечисляемого - который имеет эффект кэширования и остановки множественного перечисления оператора Linq, который является то, что ToArray() и ToList() используются для много, даже если атрибуты коллекции списка или массива никогда не используются.

Memoize доступен в системе RX/.Интерактивный lib, и объясняется здесь: больше LINQ с Система.Интерактивный

(от блог Барта Де'мета что это очень рекомендуется читать, если вы работаете с Linq для объектов много)


я обнаружил, что другие критерии, которые люди сделали здесь, отсутствуют, так что вот моя трещина в этом. Дайте мне знать, если вы найдете что-то не так с моей методологией.

/* This is a benchmarking template I use in LINQPad when I want to do a
 * quick performance test. Just give it a couple of actions to test and
 * it will give you a pretty good idea of how long they take compared
 * to one another. It's not perfect: You can expect a 3% error margin
 * under ideal circumstances. But if you're not going to improve
 * performance by more than 3%, you probably don't care anyway.*/
void Main()
{
    // Enter setup code here
    var values = Enumerable.Range(1, 100000)
        .Select(i => i.ToString())
        .ToArray()
        .Select(i => i);
    values.GetType().Dump();
    var actions = new[]
    {
        new TimedAction("ToList", () =>
        {
            values.ToList();
        }),
        new TimedAction("ToArray", () =>
        {
            values.ToArray();
        }),
        new TimedAction("Control", () =>
        {
            foreach (var element in values)
            {
                // do nothing
            }
        }),
        // Add tests as desired
    };
    const int TimesToRun = 1000; // Tweak this as necessary
    TimeActions(TimesToRun, actions);
}


#region timer helper methods
// Define other methods and classes here
public void TimeActions(int iterations, params TimedAction[] actions)
{
    Stopwatch s = new Stopwatch();
    int length = actions.Length;
    var results = new ActionResult[actions.Length];
    // Perform the actions in their initial order.
    for (int i = 0; i < length; i++)
    {
        var action = actions[i];
        var result = results[i] = new ActionResult { Message = action.Message };
        // Do a dry run to get things ramped up/cached
        result.DryRun1 = s.Time(action.Action, 10);
        result.FullRun1 = s.Time(action.Action, iterations);
    }
    // Perform the actions in reverse order.
    for (int i = length - 1; i >= 0; i--)
    {
        var action = actions[i];
        var result = results[i];
        // Do a dry run to get things ramped up/cached
        result.DryRun2 = s.Time(action.Action, 10);
        result.FullRun2 = s.Time(action.Action, iterations);
    }
    results.Dump();
}

public class ActionResult
{
    public string Message { get; set; }
    public double DryRun1 { get; set; }
    public double DryRun2 { get; set; }
    public double FullRun1 { get; set; }
    public double FullRun2 { get; set; }
}

public class TimedAction
{
    public TimedAction(string message, Action action)
    {
        Message = message;
        Action = action;
    }
    public string Message { get; private set; }
    public Action Action { get; private set; }
}

public static class StopwatchExtensions
{
    public static double Time(this Stopwatch sw, Action action, int iterations)
    {
        sw.Restart();
        for (int i = 0; i < iterations; i++)
        {
            action();
        }
        sw.Stop();

        return sw.Elapsed.TotalMilliseconds;
    }
}
#endregion

вы можете скачать скрипт LINQPad здесь.

результаты: ToArray vs ToList performance

настройка кода выше, вы обнаружите, что:

  1. разница менее значительна, когда дело с меньшим массивы. More iterations, but smaller arrays
  2. разница менее значительна при работе с intы, а не strings.
  3. используя большие structs вместо strings занимает намного больше времени в целом, но на самом деле не сильно меняет соотношение.

это согласуется с выводами топ-проголосовавших ответов:

  1. вы вряд ли заметите разницу в производительности, если ваш код часто производить много большие списки данных. (При создании 1000 списков по 100 тыс. строк каждая разница составляла всего 200 мс.)
  2. ToList() последовательно работает быстрее, и было бы лучшим выбором, если вы не планируете висеть на результатах в течение длительного времени.

обновление

@JonHanna отметил, что в зависимости от реализации Select это возможно для ToList() или ToArray() реализация для прогнозирования размера результирующей коллекции заранее. Замена .Select(i => i) в коде выше с Where(i => true) дает очень похожие результаты на данный момент и, скорее всего, сделает это независимо от реализации .NET.

Benchmark using Where instead of Select


один из вариантов-добавить свой собственный метод расширения, который возвращает только для чтения ICollection<T>. Это может быть лучше, чем использование ToList или ToArray если вы не хотите использовать свойства индексирования массива/списка или добавлять / удалять из списка.

public static class EnumerableExtension
{
    /// <summary>
    /// Causes immediate evaluation of the linq but only if required.
    /// As it returns a readonly ICollection, is better than using ToList or ToArray
    /// when you do not want to use the indexing properties of an IList, or add to the collection.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="enumerable"></param>
    /// <returns>Readonly collection</returns>
    public static ICollection<T> Evaluate<T>(this IEnumerable<T> enumerable)
    {
        //if it's already a readonly collection, use it
        var collection = enumerable as ICollection<T>;
        if ((collection != null) && collection.IsReadOnly)
        {
            return collection;
        }
        //or make a new collection
        return enumerable.ToList().AsReadOnly();
    }
}

юнит-тесты:

[TestClass]
public sealed class EvaluateLinqTests
{
    [TestMethod]
    public void EvalTest()
    {
        var list = new List<int> {1, 2, 3};
        var linqResult = list.Select(i => i);
        var linqResultEvaluated = list.Select(i => i).Evaluate();
        list.Clear();
        Assert.AreEqual(0, linqResult.Count());
        //even though we have cleared the underlying list, the evaluated list does not change
        Assert.AreEqual(3, linqResultEvaluated.Count());
    }

    [TestMethod]
    public void DoesNotSaveCreatingListWhenHasListTest()
    {
        var list = new List<int> {1, 2, 3};
        var linqResultEvaluated = list.Evaluate();
        //list is not readonly, so we expect a new list
        Assert.AreNotSame(list, linqResultEvaluated);
    }

    [TestMethod]
    public void SavesCreatingListWhenHasReadonlyListTest()
    {
        var list = new List<int> {1, 2, 3}.AsReadOnly();
        var linqResultEvaluated = list.Evaluate();
        //list is readonly, so we don't expect a new list
        Assert.AreSame(list, linqResultEvaluated);
    }

    [TestMethod]
    public void SavesCreatingListWhenHasArrayTest()
    {
        var list = new[] {1, 2, 3};
        var linqResultEvaluated = list.Evaluate();
        //arrays are readonly (wrt ICollection<T> interface), so we don't expect a new object
        Assert.AreSame(list, linqResultEvaluated);
    }

    [TestMethod]
    [ExpectedException(typeof (NotSupportedException))]
    public void CantAddToResultTest()
    {
        var list = new List<int> {1, 2, 3};
        var linqResultEvaluated = list.Evaluate();
        Assert.AreNotSame(list, linqResultEvaluated);
        linqResultEvaluated.Add(4);
    }

    [TestMethod]
    [ExpectedException(typeof (NotSupportedException))]
    public void CantRemoveFromResultTest()
    {
        var list = new List<int> {1, 2, 3};
        var linqResultEvaluated = list.Evaluate();
        Assert.AreNotSame(list, linqResultEvaluated);
        linqResultEvaluated.Remove(1);
    }
}

старый вопрос, но новые вопрошающие все время.

согласно источнику


для тех, кто заинтересован в использовании этого результата в другом Linq-to-sql, таком как

from q in context.MyTable
where myListOrArray.Contains(q.someID)
select q;

тогда генерируемый SQL будет одинаковым, независимо от того, используете ли Вы список или массив для myListOrArray. Теперь я знаю, что некоторые могут спросить, почему даже перечислять перед этим оператором, но есть разница между SQL, сгенерированным из IQueryable vs (List или Array).


ToListAsync<T>() предпочтительнее.

в Entity Framework 6 оба метода в конечном итоге вызывают один и тот же внутренний метод, но ToArrayAsync<T>() звонки list.ToArray() В конце, который реализуется как

T[] array = new T[_size];
Array.Copy(_items, 0, array, 0, _size);
return array;

так ToArrayAsync<T>() имеет некоторые накладные расходы, тем самым ToListAsync<T>() предпочтительнее.