前言: 我之所以问这个问题,是因为我没有一个足够大的数据集和计算能力来可靠地测试它。
问题: 如果使用单线程访问/使用包含数十亿项的ConcurrentBag<T>
,它是否与List<T>
表现相似?换句话说,遍历ConcurrentBag<T>
是否比遍历List<T>
更或更少效率高?
前言: 我之所以问这个问题,是因为我没有一个足够大的数据集和计算能力来可靠地测试它。
问题: 如果使用单线程访问/使用包含数十亿项的ConcurrentBag<T>
,它是否与List<T>
表现相似?换句话说,遍历ConcurrentBag<T>
是否比遍历List<T>
更或更少效率高?
ConcurrentBag<T>
不可避免地比List<T>
性能差。即使您只从单个线程访问它,该结构仍然需要采取措施防止在并发访问时出现竞争危害的可能性。ConcurrentBag(IEnumerable<T>)
构造函数而不是通过其Add
方法逐个添加每个项来避免性能开销。
ConcurrentBag<T>
为枚举提供“瞬间快照”语义;请参见其GetEnumerator
方法的备注。当您从foreach
循环中访问ConcurrentBag<T>
时,它会先将其整个内容复制到普通的List<T>
中,然后枚举该内容。这将每次在循环中使用它都产生重大的性能开销(在计算和内存方面)。List<T>
。十亿个项目应该选择List还是ConcurrentBag?答案是否定的。
关于性能问题,可以尝试以下测试添加操作的代码(如果需要可以自行修改以测试其他操作):
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace ConcurrentBagTest
{
// You must compile this for x64 or you will get OutOfMemory exception
class Program
{
static void Main(string[] args)
{
ListTest(10000000);
ListTest(100000000);
ListTest(1000000000);
ConcurrentBagTest(10000000);
ConcurrentBagTest(100000000);
Console.ReadKey();
}
static void ConcurrentBagTest(long count)
{
try
{
var bag = new ConcurrentBag<long>();
Console.WriteLine($"--- ConcurrentBagTest count = {count}");
Console.WriteLine($"I will use {(count * sizeof(long)) / Math.Pow(1024, 2)} MiB of RAM");
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
for (long i = 0; i < count; i++)
{
bag.Add(i);
}
stopwatch.Stop();
Console.WriteLine($"Inserted {bag.LongCount()} items in {stopwatch.Elapsed.TotalSeconds} s");
Console.WriteLine();
Console.WriteLine();
}
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
}
GC.Collect();
GC.WaitForPendingFinalizers();
}
static void ListTest(long count)
{
try
{
var list = new List<long>();
Console.WriteLine($"--- ListTest count = {count}");
Console.WriteLine($"I will use {(count * sizeof(long)) / Math.Pow(1024, 2)} MiB of RAM");
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
for (long i = 0; i < count; i++)
{
list.Add(i);
}
stopwatch.Stop();
Console.WriteLine($"Inserted {list.LongCount()} items in {stopwatch.Elapsed.TotalSeconds} s");
Console.WriteLine();
Console.WriteLine();
}
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
}
GC.Collect();
GC.WaitForPendingFinalizers();
}
}
}
我的输出:
--- ListTest count = 10000000
I will use 76,2939453125 MiB of RAM
Inserted 10000000 items in 0,0807315 s
--- ListTest count = 100000000
I will use 762,939453125 MiB of RAM
Inserted 100000000 items in 0,7741546 s
--- ListTest count = 1000000000
I will use 7629,39453125 MiB of RAM
System.OutOfMemoryException: Array dimensions exceeded supported range.
--- ConcurrentBagTest count = 10000000
I will use 76,2939453125 MiB of RAM
Inserted 10000000 items in 1,0744069 s
--- ConcurrentBagTest count = 100000000
I will use 762,939453125 MiB of RAM
Inserted 100000000 items in 11,3976436 s
使用的CPU:Intel Core i7-2600 @ 3.4 GHz,
使用的RAM:16 GB
同时查看此答案以了解限制。
然而,如果您需要删除项目,ConcurrentBag比List快得多。
原始答案翻译成:最初的回答
void Main()
{
ConcurrentBag<int> bag = new ConcurrentBag<int>();
ConcurrentStack<int> stack = new ConcurrentStack<int>();
ConcurrentQueue<int> q = new ConcurrentQueue<int>();
List<int> list = new List<int>();
Stopwatch sw = new Stopwatch();
int count = 100000;
sw.Start();
for (int i = 0; i < count; i++)
{
bag.Add(i);
}
for (int i = 0; i< count; i++)
{
bag.TryTake(out _);
}
sw.Elapsed.Dump("BAG");
sw.Start();
for (int i = 0; i < count; i++)
{
stack.Push(i);
}
for (int i = 0; i < count; i++)
{
stack.TryPop(out _);
}
sw.Elapsed.Dump("Stack");
sw.Start();
for (int i = 0; i < count; i++)
{
q.Enqueue(i);
}
for (int i = 0; i < count; i++)
{
q.TryDequeue(out _);
}
sw.Elapsed.Dump("Q");
sw.Start();
for (int i = 0; i < count; i++)
{
list.Add(i);
}
for (int i = 0; i < count; i++)
{
list.RemoveAt(0);
}
sw.Elapsed.Dump("list remove at 0");
sw.Start();
for (int i = 0; i < count; i++)
{
list.Add(i);
}
for (int i = 0; i < count; i++)
{
list.RemoveAt(list.Count -1);
}
sw.Elapsed.Dump("list remove at end");
}
结果:
BAG 00:00:00.0144421
栈 00:00:00.0341379
队列 00:00:00.0400114
列表删除第一个元素 00:00:00.6188329
列表删除最后一个元素 00:00:00.6202170
Restart
计时器,而不仅仅是 Start
它。从 List
的末尾删除实际上是最快的方法(https://dotnetfiddle.net/Erbzp4)。 - Theodor Zoulias
foreach
一样进行迭代。 - Leonardo