Лучше ли вызывать ToList () или ToArray () в запросах LINQ? - PullRequest
463 голосов
/ 09 июля 2009

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

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 ]

312 голосов
/ 01 мая 2013

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

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

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

EDIT

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

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

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

Ключевым моментом здесь является профиль, профиль, а затем профиль еще.

161 голосов
/ 09 июля 2009

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

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

40 голосов
/ 20 декабря 2016

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

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

Этот пост является просто дополнением к упоминанию семантической разницы , которая существует между 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 поговорка:

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

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

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

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

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

26 голосов
/ 12 января 2011

Я согласен с @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 элементов. Таким образом, вы можете видеть, что разница во времени и в памяти незначительна.

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

19 голосов
/ 01 февраля 2010

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

19 голосов
/ 09 июля 2009

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

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

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

15 голосов
/ 12 июля 2010

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

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

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

Однако я использовал Reflector, чтобы взглянуть на методы в пространстве имен расширений System.Linq.Enumerable, и заметил очень распространенную оптимизацию.
По мере возможности источник IEnumerable<T> приводится к IList<T> или ICollection<T> для оптимизации метода. Например, посмотрите на ElementAt(int).

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

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

12 голосов
/ 08 октября 2013

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

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

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

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

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

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

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

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

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

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

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

9 голосов
/ 08 сентября 2017

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

/* 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 с, а не string с.
  3. Использование больших struct с вместо string с обычно занимает намного больше времени, но на самом деле не сильно меняет соотношение.

Это согласуется с выводами ответов с наибольшим количеством голосов:

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

Обновление

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

Benchmark using Where instead of Select

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...