ConcurrentBag与List的性能比较

11

前言: 我之所以问这个问题,是因为我没有一个足够大的数据集和计算能力来可靠地测试它。

问题: 如果使用单线程访问/使用包含数十亿项的ConcurrentBag<T>,它是否与List<T>表现相似?换句话说,遍历ConcurrentBag<T>是否比遍历List<T>更或更少效率高?


@LucasTrzesniewski 我的基准测试结果不确定,因为测试结果相差很大。正如问题中所提到的,我的开发环境非常弱(仅有8GB内存和i3处理器),我也没有一个好的数据集来真正地对其进行压力测试... - Leonardo
@Douglas 这个袋子可以像 foreach 一样进行迭代。 - Leonardo
你的线程在迭代集合时是否有可能添加更多的项目? - Douglas
@Douglas 在枚举开始后不会添加任何项目。 - Leonardo
3
如果你只在单个线程中使用集合,为什么要使用专门设计用于多线程访问的集合?请注意,如果不知道你实际使用集合做什么,我们无法回答这样的问题。 - Servy
显示剩余4条评论
3个回答

12
ConcurrentBag<T>不可避免地比List<T>性能差。即使您只从单个线程访问它,该结构仍然需要采取措施防止在并发访问时出现竞争危害的可能性。
如果您将从单个线程加载集合之前开始枚举,则可以通过使用ConcurrentBag(IEnumerable<T>)构造函数而不是通过其Add方法逐个添加每个项来避免性能开销。 ConcurrentBag<T>为枚举提供“瞬间快照”语义;请参见其GetEnumerator方法的备注。当您从foreach 循环中访问ConcurrentBag<T>时,它会先将其整个内容复制到普通的List<T>中,然后枚举该内容。这将每次在循环中使用它都产生重大的性能开销(在计算和内存方面)。
如果您的场景是列表将由多个线程填充,但然后仅由一个线程读取,则应尽快将其转换为List<T>

8

十亿个项目应该选择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

同时查看此答案以了解限制。


2

然而,如果您需要删除项目,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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接