刚才我看了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>
,用前面操纵是快的(-er比List<T>
),但对于结束,这是令人难以置信的缓慢,为消除操纵与结束是太。
那么......有没有Maven可以解释一下:
1.使用X1 M12 N1 X和X1 M13 N1 X的末端在性能上的相似性,
1.使用List<T>
的前端和后端的差异,以及
1.使用LinkedList<T>
的末尾 * 如此 * 慢的原因(不适用,因为这是由于使用Linq的Last()
而导致的编码错误,感谢CodesInChaos)?
我想我知道为什么List<T>
不能很好的处理前面的列表了...因为List<T>
需要来回移动整个列表。如果我错了请纠正我。
P.S.我的System.Diagnostics.Stopwatch.Frequency
是2435947
,该程序是针对.NET 4客户端配置文件,并与C# 4.0编译,在Windows 7 Visual Studio 2010.
7条答案
按热度按时间cfh9epnr1#
关于1:
Stack<T>
和List<T>
的性能相似并不奇怪,我希望它们都使用带倍增策略的数组,这会导致分摊的常数时间加法。除了it leads to less expressive code之外,您可以在任何可以使用
Stack<T>
的地方使用List<T>
。关于2:
我想我知道为什么
List<T>
不能很好地处理前端了......因为List<T>
在处理前端时需要来回移动整个列表。是的。在开头插入/删除元素是昂贵的,因为它移动了所有元素。另一方面,在开头获取或替换元素是便宜的。
关于3:
缓慢的
LinkedList<T>.RemoveLast
值是基准测试代码中的一个错误。移除或获取一个双向链表的最后一项是很便宜的,对于
LinkedList<T>
来说,这意味着RemoveLast
和Last
是很便宜的。但是你没有使用
Last
属性,而是使用LINQ的扩展方法Last()
,在没有实现IList<T>
的集合上,它迭代整个列表,给它O(n)
运行时。kh212irz2#
List<T>
是动态过度分配数组(在许多其他语言的标准库中也可以看到这种数据结构)。这意味着它在内部使用了“静态”数组(一个不能调整大小的数组,在.NET中称为“数组”),它可能并且经常大于列表的大小。然后追加只是增加一个计数器,并使用下一个先前未使用的内部数组的槽。只有当内部数组变得太小而无法容纳所有项时,才会重新分配数组(这需要复制所有元素)。当发生这种情况时,数组的大小将增加一个因子(不是常数),通常为2。这确保了“分摊”时间复杂度(基本上,在长操作序列上每个操作的平均时间)是O(1)即使在最坏的情况下。对于在前面添加,这样的优化是不可行的(至少在保持随机访问和O(1)在末尾追加的情况下不会)。它总是必须复制所有元素以将它们移动到它们的新槽中(在第一个插槽中为添加的元素腾出空间)。
Stack<T>
做了同样的事情,您只是没有注意到添加到前面的差异,因为您只在一端(快速的一端)操作。获取链表的末尾在很大程度上取决于链表的内部结构。一个 * 可以 * 维护对最后一个元素的引用,但是这使得链表上的所有操作更加复杂,并且可能(我手边没有例子)会使一些操作的开销更大,缺少这样的引用,追加到末尾需要遍历链表的 * 所有 * 元素以找到最后一个节点,这对于非平凡大小的列表来说当然非常慢。
正如@CodesInChaos所指出的,您的链表操作存在缺陷。您现在看到的快速检索结尾很可能是由于
LinkedList<T>
显式维护了对最后一个节点的引用,如上所述。请注意,获取不在任何一端的元素仍然很慢。vxqlmq5t3#
速度主要来自于插入、删除或搜索一个项目所需的操作数量。您已经注意到,该列表需要内存传输。
堆栈是一个列表,只能访问顶部元素--计算机总是知道它在哪里。
链表是另一回事:列表的开始是已知的,因此从开始添加或删除非常快--但是找到最后一个元素需要时间。缓存最后一个元素的位置OTOH只值得添加。对于删除,需要遍历减去一个元素的完整列表以找到指向最后一个元素的“钩子”或指针。
只要看看这些数字,就可以对每个数据结构的内部结构做出一些有根据的猜测:
cgvd09ve4#
使用Stack和End of List在性能上的相似性,
正如delnan所解释的那样,它们都在内部使用了一个简单的数组,所以它们在最后工作时的行为非常相似,你可以看到堆栈是一个列表,只访问最后一个对象。
使用List的前端和后端的区别
你已经猜对了。操作一个列表的开头,意味着底层数组需要改变。添加一个项通常意味着你需要将所有其他元素移动一位,移除也一样。如果你知道你将操作列表的两端,你最好使用链表。
为什么使用LinkedIn列表的结尾会这么慢?
通常情况下,链表在任意位置的元素插入和删除都可以在恒定时间内完成,因为你最多只需要改变两个指针。问题是如何到达位置。一个普通的链表只有一个指向它的第一个元素的指针。所以如果你想到达最后一个元素,你需要遍历所有的元素。2用链表实现的队列通常通过增加一个指向最后一个元素的指针来解决这个问题,所以添加元素也可以在恒定时间内完成。2更复杂的数据结构是一个双向链表,它既有指向第一个 * 元素的指针,也有指向最后一个 * 元素的指针,而且每个元素还包含指向下一个 * 元素和前一个 * 元素的指针。
您应该了解的是,有许多不同的数据结构都是为了同一个目的而创建的,它们可以非常有效地处理这些目的。选择正确的结构在很大程度上取决于您想要做什么。
isr3a4wc5#
我有Java背景,我猜你的问题更多地涉及到一般的数据结构,而不是特定的语言。另外,如果我的陈述不正确,我道歉。
1.使用Stack和List结尾的性能相似
2.使用List前端和后端的差异,以及
至少在Java中,Stacks是使用arrays实现的(如果C#不是这样,请道歉。您可以参考实现的源代码),Lists也是如此。通常,对于数组,所有在末尾的插入所花费的时间都比在开头的要少,因为数组中预先存在的值需要下移,以适应在开头的插入。
Link to Stack.java source及其超类向量
3.使用LinkedList的结尾如此缓慢的原因是什么?
LinkedList不允许随机访问并且在到达插入点之前必须遍历节点。如果您发现最后一个节点的性能比较慢,那么我认为LinkedList实现应该是一个单链表。我猜您可能希望在访问最后的元素时考虑使用双链表以获得最佳性能。
http://en.wikipedia.org/wiki/Linked_list
omjgkv6w6#
只是改进了之前代码的一些不足,特别是随机和虚拟计算的影响。数组仍然是最重要的,但列表的性能令人印象深刻,LinkedIn列表非常适合随机插入。
排序结果为:
代码为:
zbdgwd5y7#
我很晚才开始讨论这个问题,但是有一个BoundedChannel对我来说非常适合保持一个滑动窗口缓冲区。
https://learn.microsoft.com/en-us/dotnet/core/extensions/channels