60

刚才我读了一些关于vs的帖子List<T>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>and的性能Queue<T>快速且具有可比性,这是意料之中的。

因为List<T>,使用前端和后端有很大的不同!令我惊讶的是,从末尾添加/删除的性能实际上与Stack<T>.

因为LinkedList<T>, 用前面的操作很快(-er 比List<T>,但到最后,用结尾的操作也非常慢


所以......任何专家都可以解释:

  1. 使用Stack<T>和结束的性能相似List<T>
  2. 使用前面和结尾的区别List<T>,以及
  3. 使用 end 如此缓慢的原因LinkedList<T>(不适用因为使用 Linq 导致的编码错误Last()感谢 CodesInChaos)?

我想我知道为什么List<T>不能很好地处理前面......因为List<T>这样做时需要来回移动整个列表。如果我错了,请纠正我。

PS MySystem.Diagnostics.Stopwatch.Frequency2435947,该程序针对 .NET 4 Client Profile 并在 Windows 7 Visual Studio 2010 上使用 C# 4.0 编译。

4

6 回答 6

40

关于1:

Stack<T>'s 和List<T>'s 的表现相似并不奇怪。我希望他们俩都使用具有加倍策略的数组。这导致摊销的恒定时间增加。

您可以在任何可以使用的List<T>地方使用Stack<T>,但这会导致代码表现力较差

关于2:

我想我知道为什么List<T>不能很好地处理前面......因为List<T>这样做时需要来回移动整个列表。

这是正确的。在开头插入/删除元素是昂贵的,因为它会移动所有元素。另一方面,在开始时获取或替换元素很便宜。

关于3:

你的慢LinkedList<T>.RemoveLast值是你的基准代码中的一个错误。

删除或获取双向链表的最后一项很便宜。在这种情况下,LinkedList<T>这意味着RemoveLast并且Last很便宜。

但是您没有使用该Last属性,而是使用 LINQ 的扩展方法Last()。在没有实现IList<T>它的集合上,它迭代整个列表,给它O(n)运行时间。

于 2012-11-03T17:11:16.577 回答
13

List<T>是一个动态的过度分配数组(在许多其他语言的标准库中也可以看到这种数据结构)。这意味着它在内部使用了一个“静态”数组(一个无法调整大小的数组,在 .NET 中称为“数组”),它可能并且通常大于列表的大小。然后追加简单地增加一个计数器并使用内部数组的下一个以前未使用的插槽。如果内部数组变得很小以容纳所有项目,则仅重新分配数组(这需要复制所有元素)。发生这种情况时,数组的大小会增加一个因子(不是常数),通常是 2。

这确保了即使在最坏的情况下,用于追加的分摊时间复杂度(基本上,每个操作在长操作序列中的平均时间)也是 O(1)。对于在前面添加,没有这样的优化是可行的(至少在同时保持随机访问和 O(1) 附加在末尾时不可行)。它总是必须复制所有元素以将它们移动到新的插槽中(为第一个插槽中添加的元素腾出空间)。Stack<T> 做同样的事情,你只是没有注意到添加到前面的差异,因为你只在一端操作(快速的一端)。

获取链表的结尾在很大程度上取决于列表的内部结构。可以维护对最后一个元素的引用,但这会使列表上的所有操作更加复杂,并且可能(我手头没有示例)会使某些操作变得更加昂贵。缺少这样的引用,追加到末尾需要遍历链表的所有元素以找到最后一个节点,这对于非平凡大小的列表来说当然非常慢。

正如@CodesInChaos 所指出的,您的链表操作存在缺陷。如上所述,您现在看到的结尾的快速检索很可能是由LinkedList<T>显式维护对最后一个节点的引用引起的。请注意,获取不在任一端的元素仍然很慢。

于 2012-11-03T17:06:20.757 回答
5

速度主要来自插入、删除或搜索项目所需的操作数量。您已经注意到,该列表需要内存传输。

堆栈是一个列表,只能在顶部元素访问——计算机总是知道它在哪里。

链表是另一回事:列表的开头是已知的,因此从开头添加或删除非常快——但找到最后一个元素需要时间。缓存最后一个元素OTOH的位置只值得添加。对于删除,需要遍历完整列表减去一个元素以找到“钩子”或指向最后一个元素的指针。

只看数字,就可以对每种数据结构的内部进行一些有根据的猜测:

  • 正如预期的那样,从堆栈中弹出很快
  • 推入堆栈速度较慢。它比添加到列表末尾要慢。为什么?
    • 显然,堆栈的分配单元大小更小——它可能只会将堆栈大小增加 100,而列表的增长可以以 1000 为单位完成。
  • 列表似乎是一个静态数组。访问前面的列表需要内存传输,这需要与列表长度成比例的时间。
  • 基本的链表操作不应该花那么多时间,通常只需要
    • new_item.next = list_start; list_start = new_item; // 加上
    • list_start = list_start.next;// 去除
    • 但是,由于 addLast 非常快,这意味着在添加或删除链表时,也必须更新指向最后一个元素的指针。所以有额外的簿记。
  • 双向链表 OTOH 使得在列表两端插入和删除的速度相对较快(我被告知更好的代码使用 DLL),但是,
    • 上一个和下一个项目的链接也使簿记工作加倍
于 2012-11-03T17:03:47.857 回答
1

我有 Java 背景,我想您的问题更多地与一般数据结构有关,而不是与特定语言有关。另外,如果我的陈述不正确,我深表歉意。

1. 使用 Stack 和 List 结尾在性能上的相似性

2. List 前后使用的区别,以及

至少在 Java 中,堆栈是使用数组实现的(如果 C# 不是这种情况,请致歉。您可以参考实现的源代码)列表也是如此。典型的数组,末尾的所有插入都比开头花费的时间更少,因为数组中预先存在的值需要向下移动以适应开头的插入。

链接到 Stack.java 源代码及其超类Vector

3. 使用LinkedList的末尾这么慢的原因是什么?

LinkedList不允许随机访问,并且必须在到达插入点之前遍历节点。如果您发现最后一个节点的性能较慢,那么我认为 LinkedList 实现应该是一个单链表。我猜你会想在最后访问元素时考虑一个双向链表以获得最佳性能。

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

于 2012-11-03T17:03:31.157 回答
1

使用 Stack 和 List 结尾在性能上的相似性,

正如 delnan 所解释的,它们都在内部使用了一个简单的数组,因此它们在最后工作时的行为非常相似。您可以看到一个堆栈是一个仅访问最后一个对象的列表。

List 前后使用的区别

你已经猜对了。操作列表的开头意味着需要更改底层数组。添加项目通常意味着您需要将所有其他元素移动一个,与删除相同。如果您知道您将操作列表的两端,则最好使用链表。

使用 LinkedList 的末尾这么慢的原因是什么?

通常,链表在任意位置的元素插入和删除都可以在恒定时间内完成,因为您最多只需要更改两个指针。问题只是到了这个位置。一个普通的链表只有一个指向它的第一个元素的指针。因此,如果要到达最后一个元素,则需要遍历所有元素。用链表实现的队列通常通过有一个指向最后一个元素的附加指针来解决这个问题,因此也可以在恒定时间内添加元素。更复杂的数据结构将是一个双链表,它具有指向第一个和最后一个元素的指针,并且每个元素还包含指向下一个和前一个元素的指针。

您应该了解的是,有许多不同的数据结构是为单一目的而设计的,它们可以非常有效地处理这些数据结构。选择正确的结构很大程度上取决于您想要做什么。

于 2012-11-03T17:24:53.243 回答
0

只是改进了之前代码的一些不足,特别是Random和dummy计算的影响。Array 仍然居于首位,但 List 的性能令人印象深刻,而且 LinkedList 非常适合随机插入。

排序后的结果是:

12      array[i]
40      list2[i]
62      FillArray
68      list2.RemoveAt
78      stack.Pop
126     list2.Add
127     queue.Dequeue
159     stack.Push
161     foreach_linkedlist1
191     queue.Enqueue
218     linkedlist1.RemoveFirst
219     linkedlist2.RemoveLast
2470        linkedlist2.AddLast
2940        linkedlist1.AddFirst

代码是:

using System;
using System.Collections.Generic;
using System.Diagnostics;
//
namespace Benchmarking {
    //
    static class Collections {
        //
        public static void Main() {
            const int limit = 9000000;
            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}  ", "stack.Push");
            sw.Start();
            for ( int i = 0; i < limit; i++ ) {
                stack.Push( i );
            }
            sw.Stop();
            Console.WriteLine( sw.ElapsedMilliseconds.ToString() );
            sw.Reset();
            Console.Write( "{0,40}  ", "stack.Pop" );
            sw.Start();
            for ( int i = 0; i < limit; i++ ) {
                stack.Pop();
            }
            sw.Stop();
            Console.WriteLine( sw.ElapsedMilliseconds.ToString() );


            sw.Reset();
            Console.Write( "{0,40}  ", "queue.Enqueue" );
            sw.Start();
            for ( int i = 0; i < limit; i++ ) {
                queue.Enqueue( i );
            }
            sw.Stop();
            Console.WriteLine( sw.ElapsedMilliseconds.ToString() );
            sw.Reset();
            Console.Write( "{0,40}  ", "queue.Dequeue" );
            sw.Start();
            for ( int i = 0; i < limit; i++ ) {
                queue.Dequeue();
            }
            sw.Stop();
            Console.WriteLine( sw.ElapsedMilliseconds.ToString() );

            //sw.Reset();
            //Console.Write( "{0,40}  ", "Insert to List at the front..." );
            //sw.Start();
            //for ( int i = 0; i < limit; i++ ) {
            //  list1.Insert( 0, i );
            //}
            //sw.Stop();
            //Console.WriteLine( sw.ElapsedMilliseconds.ToString() );
            //
            //sw.Reset();
            //Console.Write( "{0,40}  ", "RemoveAt from List at the front..." );
            //sw.Start();
            //for ( int i = 0; i < limit; i++ ) {
            //  dummy = list1[ 0 ];
            //  list1.RemoveAt( 0 );
            //  dummy++;
            //}
            //sw.Stop();
            //Console.WriteLine( sw.ElapsedMilliseconds.ToString() );

            sw.Reset();
            Console.Write( "{0,40}  ", "list2.Add" );
            sw.Start();
            for ( int i = 0; i < limit; i++ ) {
                list2.Add( i );
            }
            sw.Stop();
            Console.WriteLine( sw.ElapsedMilliseconds.ToString() );
            sw.Reset();
            Console.Write( "{0,40}  ", "list2.RemoveAt" );
            sw.Start();
            for ( int i = 0; i < limit; i++ ) {
                list2.RemoveAt( list2.Count - 1 );
            }
            sw.Stop();
            Console.WriteLine( sw.ElapsedMilliseconds.ToString() );


            sw.Reset();
            Console.Write( "{0,40}  ", "linkedlist1.AddFirst" );
            sw.Start();
            for ( int i = 0; i < limit; i++ ) {
                linkedlist1.AddFirst( i );
            }
            sw.Stop();
            Console.WriteLine( sw.ElapsedMilliseconds.ToString() );
            sw.Reset();
            Console.Write( "{0,40}  ", "linkedlist1.RemoveFirst" );
            sw.Start();
            for ( int i = 0; i < limit; i++ ) {
                linkedlist1.RemoveFirst();
            }
            sw.Stop();
            Console.WriteLine( sw.ElapsedMilliseconds.ToString() );


            sw.Reset();
            Console.Write( "{0,40}  ", "linkedlist2.AddLast" );
            sw.Start();
            for ( int i = 0; i < limit; i++ ) {
                linkedlist2.AddLast( i );
            }
            sw.Stop();
            Console.WriteLine( sw.ElapsedMilliseconds.ToString() );
            sw.Reset();
            Console.Write( "{0,40}  ", "linkedlist2.RemoveLast" );
            sw.Start();
            for ( int i = 0; i < limit; i++ ) {
                linkedlist2.RemoveLast();
            }
            sw.Stop();
            Console.WriteLine( sw.ElapsedMilliseconds.ToString() );


            // Fill again
            for ( int i = 0; i < limit; i++ ) {
                list2.Add( i );
            }
            sw.Reset();
            Console.Write( "{0,40}  ", "list2[i]" );
            sw.Start();
            for ( int i = 0; i < limit; i++ ) {
                dummy = list2[ i ];
            }
            sw.Stop();
            Console.WriteLine( sw.ElapsedMilliseconds.ToString() );


            // Fill array
            sw.Reset();
            Console.Write( "{0,40}  ", "FillArray" );
            sw.Start();
            var array = new int[ limit ];
            for ( int i = 0; i < limit; i++ ) {
                array[ i ] = i;
            }
            sw.Stop();
            Console.WriteLine( sw.ElapsedMilliseconds.ToString() );

            sw.Reset();
            Console.Write( "{0,40}  ", "array[i]" );
            sw.Start();
            for ( int i = 0; i < limit; i++ ) {
                dummy = array[ i ];
            }
            sw.Stop();
            Console.WriteLine( sw.ElapsedMilliseconds.ToString() );


            // Fill again
            for ( int i = 0; i < limit; i++ ) {
                linkedlist1.AddFirst( i );
            }
            sw.Reset();
            Console.Write( "{0,40}  ", "foreach_linkedlist1" );
            sw.Start();
            foreach ( var item in linkedlist1 ) {
                dummy = item;
            }
            sw.Stop();
            Console.WriteLine( sw.ElapsedMilliseconds.ToString() );

            //
            Console.WriteLine( "Press Enter to end." );
            Console.ReadLine();
        }
    }
}
于 2018-08-06T07:37:42.263 回答