Подтвердить что ты не робот

Различия в производительности... настолько драматичны?

Как раз сейчас я читаю несколько сообщений о List<T> vs LinkedList<T>, поэтому я решил сами оценить некоторые структуры. Я сравнивал Stack<T>, Queue<T>, List<T> и LinkedList<T>, добавляя данные и удаляя данные в/из фронта/конца. Здесь результат теста:

               Pushing to Stack...  Time used:      7067 ticks
              Poping from Stack...  Time used:      2508 ticks

               Enqueue to Queue...  Time used:      7509 ticks
             Dequeue from Queue...  Time used:      2973 ticks

    Insert to List at the front...  Time used:   5211897 ticks
RemoveAt from List at the front...  Time used:   5198380 ticks

         Add to List at the end...  Time used:      5691 ticks
  RemoveAt from List at the end...  Time used:      3484 ticks

         AddFirst to LinkedList...  Time used:     14057 ticks
    RemoveFirst from LinkedList...  Time used:      5132 ticks

          AddLast to LinkedList...  Time used:      9294 ticks
     RemoveLast from LinkedList...  Time used:      4414 ticks

Код:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;

namespace Benchmarking
{
    static class Collections
    {
        public static void run()
        {
            Random rand = new Random();
            Stopwatch sw = new Stopwatch();
            Stack<int> stack = new Stack<int>();
            Queue<int> queue = new Queue<int>();
            List<int> list1 = new List<int>();
            List<int> list2 = new List<int>();
            LinkedList<int> linkedlist1 = new LinkedList<int>();
            LinkedList<int> linkedlist2 = new LinkedList<int>();
            int dummy;


            sw.Reset();
            Console.Write("{0,40}", "Pushing to Stack...");
            sw.Start();
            for (int i = 0; i < 100000; i++)
            {
                stack.Push(rand.Next());
            }
            sw.Stop();
            Console.WriteLine("  Time used: {0,9} ticks", sw.ElapsedTicks);
            sw.Reset();
            Console.Write("{0,40}", "Poping from Stack...");
            sw.Start();
            for (int i = 0; i < 100000; i++)
            {
                dummy = stack.Pop();
                dummy++;
            }
            sw.Stop();
            Console.WriteLine("  Time used: {0,9} ticks\n", sw.ElapsedTicks);


            sw.Reset();
            Console.Write("{0,40}", "Enqueue to Queue...");
            sw.Start();
            for (int i = 0; i < 100000; i++)
            {
                queue.Enqueue(rand.Next());
            }
            sw.Stop();
            Console.WriteLine("  Time used: {0,9} ticks", sw.ElapsedTicks);
            sw.Reset();
            Console.Write("{0,40}", "Dequeue from Queue...");
            sw.Start();
            for (int i = 0; i < 100000; i++)
            {
                dummy = queue.Dequeue();
                dummy++;
            }
            sw.Stop();
            Console.WriteLine("  Time used: {0,9} ticks\n", sw.ElapsedTicks);


            sw.Reset();
            Console.Write("{0,40}", "Insert to List at the front...");
            sw.Start();
            for (int i = 0; i < 100000; i++)
            {
                list1.Insert(0, rand.Next());
            }
            sw.Stop();
            Console.WriteLine("  Time used: {0,9} ticks", sw.ElapsedTicks);
            sw.Reset();
            Console.Write("{0,40}", "RemoveAt from List at the front...");
            sw.Start();
            for (int i = 0; i < 100000; i++)
            {
                dummy = list1[0];
                list1.RemoveAt(0);
                dummy++;
            }
            sw.Stop();
            Console.WriteLine("  Time used: {0,9} ticks\n", sw.ElapsedTicks);


            sw.Reset();
            Console.Write("{0,40}", "Add to List at the end...");
            sw.Start();
            for (int i = 0; i < 100000; i++)
            {
                list2.Add(rand.Next());
            }
            sw.Stop();
            Console.WriteLine("  Time used: {0,9} ticks", sw.ElapsedTicks);
            sw.Reset();
            Console.Write("{0,40}", "RemoveAt from List at the end...");
            sw.Start();
            for (int i = 0; i < 100000; i++)
            {
                dummy = list2[list2.Count - 1];
                list2.RemoveAt(list2.Count - 1);
                dummy++;
            }
            sw.Stop();
            Console.WriteLine("  Time used: {0,9} ticks\n", sw.ElapsedTicks);


            sw.Reset();
            Console.Write("{0,40}", "AddFirst to LinkedList...");
            sw.Start();
            for (int i = 0; i < 100000; i++)
            {
                linkedlist1.AddFirst(rand.Next());
            }
            sw.Stop();
            Console.WriteLine("  Time used: {0,9} ticks", sw.ElapsedTicks);
            sw.Reset();
            Console.Write("{0,40}", "RemoveFirst from LinkedList...");
            sw.Start();
            for (int i = 0; i < 100000; i++)
            {
                dummy = linkedlist1.First.Value;
                linkedlist1.RemoveFirst();
                dummy++;
            }
            sw.Stop();
            Console.WriteLine("  Time used: {0,9} ticks\n", sw.ElapsedTicks);


            sw.Reset();
            Console.Write("{0,40}", "AddLast to LinkedList...");
            sw.Start();
            for (int i = 0; i < 100000; i++)
            {
                linkedlist2.AddLast(rand.Next());
            }
            sw.Stop();
            Console.WriteLine("  Time used: {0,9} ticks", sw.ElapsedTicks);
            sw.Reset();
            Console.Write("{0,40}", "RemoveLast from LinkedList...");
            sw.Start();
            for (int i = 0; i < 100000; i++)
            {
                dummy = linkedlist2.Last.Value;
                linkedlist2.RemoveLast();
                dummy++;
            }
            sw.Stop();
            Console.WriteLine("  Time used: {0,9} ticks\n", sw.ElapsedTicks);
        }
    }
}

Различия так драматичны!

Как вы можете видеть, производительность Stack<T> и Queue<T> является быстрой и сопоставимой, ожидаемой.

Для List<T>, использование фронта и конца имеет столько различий! И, к моему удивлению, производительность добавления/удаления с конца фактически сравнима с производительностью Stack<T>.

Для LinkedList<T>, манипуляция с фронтом выполняется быстро (-er than List<T>) , но для конца это невероятно медленно для удаления, когда обработка тоже заканчивается.


Итак... могут ли какие-либо эксперты учитывать:

  • сходство в производительности использования Stack<T> и конца List<T>,
  • различия в использовании фронта и конца List<T> и
  • причина, заключающаяся в том, что использование конца LinkedList<T> настолько медленное (неприменимо, поскольку это ошибка кодирования из-за использования Linq Last(), благодаря CodesInChaos)?

Я думаю, я знаю, почему List<T> не так хорошо справляется с фронтом... потому что List<T> должен переместить весь список назад и назад, когда это делается. Исправьте меня, если я ошибаюсь.

P.S. Мой System.Diagnostics.Stopwatch.Frequency - 2435947, и программа предназначена для .NET-профиля клиента и скомпилирована с С# 4.0 в Windows 7 Visual Studio 2010.

4b9b3361

Ответ 1

Относительно 1:

Stack<T> и List<T> производительность, подобная, не удивительна. Я ожидаю, что оба они будут использовать массивы с удвоенной стратегией. Это приводит к амортизации добавок с постоянным временем.

Вы можете использовать List<T> везде, где вы можете использовать Stack<T>, но это приводит к менее выразительному коду.

Относительно 2:

Думаю, я знаю, почему List<T> не так хорошо справляется с фронтом... потому что List<T> нужно переместить весь список назад и при этом.

Это правильно. Вставка/удаление элементов в начале является дорогостоящей, поскольку она перемещает все элементы. С другой стороны, получение или замена элементов в начале дешево.

Относительно 3:

Ваше медленное значение LinkedList<T>.RemoveLast является ошибкой в ​​вашем бенчмаркинговом коде.

Удаление или получение последнего элемента двусвязного списка дешево. В случае LinkedList<T> это означает, что RemoveLast и Last являются дешевыми.

Но вы не использовали свойство Last, а метод расширения LINQ Last(). В коллекциях, которые не реализуют IList<T>, он выполняет итерацию всего списка, предоставляя ему O(n) время выполнения.

Ответ 2

List<T> является динамическим массивом перенаправления (структура данных, которую вы также увидите в стандартной библиотеке многих других языков). Это означает, что он внутренне использует "статический" массив (массив, который нельзя изменить, известный как "массив" в .NET), который может быть и часто больше, чем размер списка. Затем добавление просто увеличивает счетчик и использует следующий, ранее неиспользуемый слот внутреннего массива. Массив перераспределяется (требуется копирование всех элементов), если внутренний массив становится малым для размещения всех элементов. Когда это произойдет, размер массива увеличивается на факторы (не константы), обычно 2.

Это гарантирует, что усредненная временная сложность (в основном, среднее время на операцию над длинной последовательностью операций) для добавления - O (1) даже в худшем случае. Для добавления на фронт такая оптимизация невозможна (по крайней мере, не сохраняя при этом как произвольный доступ, так и добавление O (1) в конце). Всегда нужно скопировать все элементы, чтобы переместить их в свои новые слоты (создавая пространство для добавленного элемента в первом слоте). Stack<T> делает то же самое, вы просто не замечаете несоответствия с добавлением фронта, потому что вы только когда-либо работаете на одном конце (быстрый).

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

Как указано @CodesInChaos, ваша связанная манипуляция списком была ошибочной. Быстрый поиск конца, который вы видите сейчас, скорее всего вызван тем, что LinkedList<T> явно поддерживает ссылку на последний node, как упоминалось выше. Обратите внимание, что получение элемента не с обоих концов все еще медленное.

Ответ 3

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

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

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

Просто глядя на цифры, можно сделать некоторые обоснованные предположения о внутренних компонентах каждой структуры данных:

  • Поп из стека быстро, как и ожидалось
  • push to stack работает медленнее. и это медленнее, чем добавление в конец списка. Зачем?
    • По-видимому, размер единицы размещения для стека меньше - он может только увеличить размер стека на 100, а увеличение списка можно выполнить в единицах 1000.
  • Список кажется статическим массивом. Для доступа к списку на передней панели требуется передача данных, что занимает время пропорционально длине списка.
  • Операции с базовыми связанными списками не должны занимать гораздо больше времени, обычно требуется только
    • new_item.next = list_start; list_start = new_item;//добавить
    • list_start = list_start.next;//удалить
    • однако, поскольку addLast работает так быстро, это означает, что при добавлении или удалении в связанном списке необходимо также обновить указатель на последний элемент. Таким образом, есть дополнительная бухгалтерия.
  • Двунаправленные списки OTOH делают его относительно быстрым для вставки и удаления на обоих концах списка (мне сообщили, что лучший код использует библиотеки DLL), однако,
    • ссылки на предыдущий и следующий элементы также удваивают работу для бухгалтерского учета.

Ответ 4

У меня есть Java-фона, и я думаю, что ваш вопрос больше связан с общими структурами данных, чем с конкретным языком. Кроме того, я извиняюсь, если мои утверждения неверны.

1. сходство в эффективности использования Stack и конца списка

2. различия в использовании фронта и конца списка, а

Как минимум в Java, стеки реализуются с использованием массивов (извиняется, если это не так с С#. Вы можете обратиться к источнику для реализации). То же самое касается списков. Типичный с массивом, все вставки в конце занимают меньше времени, чем в начале, потому что ранее существовавшие значения в массиве должны быть перемещены вниз для размещения вставки в начале.

Ссылка на источник Stack.java и его суперкласс Vector

3. причина в том, что использование конца LinkedList настолько медленное?

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

http://en.wikipedia.org/wiki/Linked_list

Ответ 5

сходство в производительности использования Stack и конца списка,

Как объясняется delnan, они оба используют простой массив внутри, поэтому они ведут себя очень схожими при работе в конце. Вы можете видеть, что стек представляет собой список, имеющий только доступ к последнему объекту.

различия в использовании фронта и конца списка

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

причина, по которой использование конца LinkedList происходит настолько медленно?

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

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