性能差异......如此戏剧化?

时间:2012-11-03 16:48:59

标签: c# .net data-structures collections

刚才我读了some posts about 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>,使用前端进行操作的速度很快(比List<T>快,但最后,删除操作结束的速度非常慢太


所以......任何专家都能说明:

  1. 使用Stack<T>List<T>的结尾
  2. 的效果相似度
  3. 使用List<T>的前端和末端的差异,以及
  4. 使用LinkedList<T>的结尾是所以的原因(不适用,因为这是因为使用了Linq的Last()感谢CodesInChaos )?
  5. 我想我知道为什么List<T>不能很好地处理前面......因为List<T>需要在执行此操作时来回移动整个列表。如果我错了,请纠正我。

    P.S。我的System.Diagnostics.Stopwatch.Frequency2435947,该程序的目标是.NET 4 Client Profile,并在Windows 7 Visual Studio 2010上使用C#4.0进行编译。

6 个答案:

答案 0 :(得分:35)

关于1:

Stack<T>List<T>的表现相似并不令人惊讶。我希望他们两个都使用具有倍增策略的数组。这导致摊销的恒定时间增加。

您可以在List<T>处使用Stack<T>it leads to less expressive code。{/ p>

关于2:

  

我想我知道为什么List<T>不能很好地处理前面...因为List<T>需要在执行此操作时来回移动整个列表。

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

关于3:

您的慢LinkedList<T>.RemoveLast值是您的基准测试代码中的错误。

删除或获取双向链表的最后一项很便宜。如果LinkedList<T>表示RemoveLastLast便宜。

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

答案 1 :(得分:13)

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

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

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

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

答案 2 :(得分:5)

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

Stack是一个列表,只能在top元素访问 - 而且计算机总是知道它在哪里。

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

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

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

答案 3 :(得分:1)

我有Java背景,我猜你的问题与一般数据结构有关,而不是特定语言。另外,如果我的陈述不正确,我会道歉。

<强> 1。使用Stack和List的结尾

的性能相似性

<强> 2。使用List的前端和末尾的差异,以及

至少在Java中,Stacks是使用数组实现的(如果C#不是这种情况,则表示道歉。您可以参考实现的来源)和Lists的情况相同。典型的数组,最后的所有插入所花费的时间都比开始时少,因为数组中预先存在的值需要向下移动以适应开头的插入。

Link to Stack.java source及其超类Vector

第3。使用LinkedList结尾这么慢的原因是什么?

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

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

答案 4 :(得分:1)

  

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

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

  

使用List

的前端和尾端的差异

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

  

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

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

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

答案 5 :(得分: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();
        }
    }
}