为什么ConcurrentBag< T>在.net(4.0)这么慢?我是不是做错了? [英] Why is ConcurrentBag<T> so slow in .Net (4.0)? Am I doing it wrong?

查看:2738
本文介绍了为什么ConcurrentBag< T>在.net(4.0)这么慢?我是不是做错了?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

在我启动了一个项目,我写了一个简单的测试,以ConcurrentBag从(System.Collections.Concurrent)相对性能比较锁定和放大器;名单。我感到非常惊讶,ConcurrentBag比用一个简单的列表锁定慢10倍以上。据我了解,在ConcurrentBag效果最好时,读写器是相同的线程。但是,我没有想到它的性能会比传统的锁差这么多。

我对循环写入和列表/袋读运行一个测试有两个平行。然而,本身写入显示出巨大的差异:

 私有静态无效ConcurrentBagTest()
   {
        INT collSize =千万;
        秒表秒表=新的秒表();
        ConcurrentBag< INT> BAG1 =新ConcurrentBag<诠释>();

        stopWatch.Start();


        的Parallel.For(0,collSize,委托(int i)以
        {
            bag1.Add(ⅰ);
        });


        stopWatch.Stop();
        Console.WriteLine(经历时间= {0},
                          stopWatch.Elapsed.TotalSeconds);
 }
 

在我的盒子,这需要3-4秒之间运行,相比于0.5 - 0.9秒的这个code:

 私有静态无效LockCollTest()
       {
        INT collSize =千万;
        对象list1_lock =新的对象();
        名单< INT> LST1 =新的名单,其中,INT>(collSize);

        秒表秒表=新的秒表();
        stopWatch.Start();


        的Parallel.For(0,collSize,委托(int i)以
            {
                锁(list1_lock)
                {
                    lst1.Add(ⅰ);
                }
            });

        stopWatch.Stop();
        Console.WriteLine(经历= {0},
                          stopWatch.Elapsed.TotalSeconds);
       }
 

正如我所提到的,在做并行读取和写入不利于并发包测试。我是不是做错了什么或者是这个数据结构真的很慢?

- 我删除了任务,因为我不需要他们在这里(满code的另一项任务读数)

非常感谢你的答案。我有一个很难挑选正确答案,因为它似乎是一些答案的组合。

正如迈克尔·Goldshteyn指出,速度实际上取决于数据。 达林指出应该有更多的争用ConcurrentBag要更快,而的Parallel.For不一定启动线程的数相同。有一点要带走的是不要做任何你不知道的必须锁里面。在上述情况下,我没有看到,除了可能的值赋给一个临时变量自己做什么锁里面。

此外,sixlettervariables指出线程碰巧运行的数目也可能影响的结果,尽管我试图运行在相反的顺序原测试和ConcurrentBag仍然较慢

我进行了一些测试,开始15任务和结果取决于除其他事项外集合大小。然而,ConcurrentBag表现几乎一样好,甚至比锁定列表,多达100万插入更好。超过100万,锁定似乎要快很多的时候,但是我可能永远不会有我的项目更大的数据结构。 这里的code,我跑:

  INT collSize = 1000000;
        对象list1_lock =新的对象();
        名单< INT> LST1 =新的名单,其中,INT>();
        ConcurrentBag< INT> concBag =新ConcurrentBag<诠释>();
        INT numTasks = 15;

        INT I = 0;

        秒表斯沃琪=新的秒表();
        sWatch.Start();
         //首先,尝试锁
        Task.WaitAll(Enumerable.Range(1,numTasks)
           。选择(X => Task.Factory.StartNew(()=>
            {
                对于(i = 0; I< collSize / numTasks;我++)
                {
                    锁(list1_lock)
                    {
                        lst1.Add(X);
                    }
                }
            }))的ToArray())。

        sWatch.Stop();
        Console.WriteLine(锁测试。消逝= {0},
            sWatch.Elapsed.TotalSeconds);

        //现在尝试concurrentBag
        sWatch.Restart();
        Task.WaitAll(Enumerable.Range(1,numTasks)。
                选择(X => Task.Factory.StartNew(()=>
            {
                对于(i = 0; I< collSize / numTasks;我++)
                {
                    concBag.Add(X);
                }
            }))的ToArray())。

        sWatch.Stop();
        Console.WriteLine(浓袋的考验。消逝= {0},
               sWatch.Elapsed.TotalSeconds);
 

解决方案

让我问你:怎样的现实是,你不得不被不断添加到集合并再从中读取应用程序?有什么用这样一个集合的? (这不是一个纯粹的反问。我的可以的想象有被使用的地方,例如,你只能从收集阅读关机(用于记录),或者当用户请求。我相信这些方案是相当罕见,虽然。)

这是你的code为模拟。呼叫名单,其中,T>。新增将是闪电般的速度,但在所有的偶然情况下列表有来调整其内部数组;但是这是平滑由所有其他补充说相当迅速发生。所以,你可能不会看到在这个背景下显著量争,尤其的个人PC机上的测试,例如,即使是8个内核(如你说你有一个注释的地方)。的也许的,你可能会看到的东西像一个24核的机器,有许多内核可尝试添加到列表中的字面上的在同一时间。

更多争

争更可能在你的悄悄从您的收藏读的,尤其。在的foreach 循环(或LINQ查询其金额的foreach 引擎盖下环),需要锁定整个运作,使你是不是修改你的收藏,同时进行迭代。

如果你能真实地再现这种情况下,我相信你会看到 ConcurrentBag< T> 规模比你目前的测试更好的展示


更新这里是一个程序,我写的比较这些集合在我上面描述的场景(多作家,不少读者)。运行25试验与10000和8个读线程集合的大小,我得到了以下结果:

花了529.0095毫秒增加10000元素到一个List<双> 8读取器线程。
拿了39.5237毫秒增加10000元到ConcurrentBag<双> 8读取器线程。
花了309.4475毫秒增加10000元素到一个List<双> 8读取器线程。
拿了81.1967毫秒增加10000元到ConcurrentBag<双> 8读取器线程。
花了228.7669毫秒增加10000元素到一个List<双> 8读取器线程。
花了164.8376毫秒增加10000元到ConcurrentBag<双> 8读取器线程。
[...]
平均上榜时间:176.072456毫秒
一般的包包时间:59.603656毫秒

所以很明显这取决于你在做什么与这些集合。

Before I started a project, I wrote a simple test to compare the performance of ConcurrentBag from (System.Collections.Concurrent) relative to locking & lists. I am extremely surprised that ConcurrentBag is over 10 times slower than locking with a simple List. From what I understand, the ConcurrentBag works best when the reader and writer is the same thread. However, I hadn't thought it's performance would be so much worse than traditional locks.

I have run a test with two Parallel for loops writing to and reading from a list/bag. However, the write by itself shows a huge difference:

private static void ConcurrentBagTest()
   {
        int collSize = 10000000;
        Stopwatch stopWatch = new Stopwatch();
        ConcurrentBag<int> bag1 = new ConcurrentBag<int>();

        stopWatch.Start();


        Parallel.For(0, collSize, delegate(int i)
        {
            bag1.Add(i);
        });


        stopWatch.Stop();
        Console.WriteLine("Elapsed Time = {0}", 
                          stopWatch.Elapsed.TotalSeconds);
 }

On my box, this takes between 3-4 secs to run, compared to 0.5 - 0.9 secs of this code:

       private static void LockCollTest()
       {
        int collSize = 10000000;
        object list1_lock=new object();
        List<int> lst1 = new List<int>(collSize);

        Stopwatch stopWatch = new Stopwatch();
        stopWatch.Start();


        Parallel.For(0, collSize, delegate(int i)
            {
                lock(list1_lock)
                {
                    lst1.Add(i);
                }
            });

        stopWatch.Stop();
        Console.WriteLine("Elapsed = {0}", 
                          stopWatch.Elapsed.TotalSeconds);
       }

As I mentioned, doing concurrent reads and writes doesn't help the concurrent bag test. Am I doing something wrong or is this data structure just really slow?

[EDIT] - I removed the Tasks because I don't need them here (Full code had another task reading)

[EDIT] Thanks a lot for the answers. I am having a hard time picking "the right answer" since it seems to be a mix of a few answers.

As Michael Goldshteyn pointed out, the speed really depends on the data. Darin pointed out that there should be more contention for ConcurrentBag to be faster, and Parallel.For doesn't necessarily start the same number of threads. One point to take away is to not do anything you don't have to inside a lock. In the above case, I don't see myself doing anything inside the lock except may be assigning the value to a temp variable.

Additionally, sixlettervariables pointed out that the number of threads that happen to be running may also affect results, although I tried running the original test in reverse order and ConcurrentBag was still slower.

I ran some tests with starting 15 Tasks and the results depended on the collection size among other things. However, ConcurrentBag performed almost as well as or better than locking a list, for up to 1 million insertions. Above 1 million, locking seemed to be much faster sometimes, but I'll probably never have a larger datastructure for my project. Here's the code I ran:

        int collSize = 1000000;
        object list1_lock=new object();
        List<int> lst1 = new List<int>();
        ConcurrentBag<int> concBag = new ConcurrentBag<int>();
        int numTasks = 15;

        int i = 0;

        Stopwatch sWatch = new Stopwatch();
        sWatch.Start();
         //First, try locks
        Task.WaitAll(Enumerable.Range(1, numTasks)
           .Select(x => Task.Factory.StartNew(() =>
            {
                for (i = 0; i < collSize / numTasks; i++)
                {
                    lock (list1_lock)
                    {
                        lst1.Add(x);
                    }
                }
            })).ToArray());

        sWatch.Stop();
        Console.WriteLine("lock test. Elapsed = {0}", 
            sWatch.Elapsed.TotalSeconds);

        // now try concurrentBag
        sWatch.Restart();
        Task.WaitAll(Enumerable.Range(1, numTasks).
                Select(x => Task.Factory.StartNew(() =>
            {
                for (i = 0; i < collSize / numTasks; i++)
                {
                    concBag.Add(x);
                }
            })).ToArray());

        sWatch.Stop();
        Console.WriteLine("Conc Bag test. Elapsed = {0}",
               sWatch.Elapsed.TotalSeconds);

解决方案

Let me ask you this: how realistic is it that you'd have an application which is constantly adding to a collection and never reading from it? What's the use of such a collection? (This is not a purely rhetorical question. I could imagine there being uses where, e.g., you only read from the collection on shutdown (for logging) or when requested by the user. I believe these scenarios are fairly rare, though.)

This is what your code is simulating. Calling List<T>.Add is going to be lightning-fast in all but the occasional case where the list has to resize its internal array; but this is smoothed out by all the other adds that happen quite quickly. So you're not likely to see a significant amount of contention in this context, especially testing on a personal PC with, e.g., even 8 cores (as you stated you have in a comment somewhere). Maybe you might see more contention on something like a 24-core machine, where many cores can be trying to add to the list literally at the same time.

Contention is much more likely to creep in where you read from your collection, esp. in foreach loops (or LINQ queries which amount to foreach loops under the hood) which require locking the entire operation so that you aren't modifying your collection while iterating over it.

If you can realistically reproduce this scenario, I believe you will see ConcurrentBag<T> scale much better than your current test is showing.


Update: Here is a program I wrote to compare these collections in the scenario I described above (multiple writers, many readers). Running 25 trials with a collection size of 10000 and 8 reader threads, I got the following results:

Took 529.0095 ms to add 10000 elements to a List<double> with 8 reader threads.
Took 39.5237 ms to add 10000 elements to a ConcurrentBag<double> with 8 reader threads.
Took 309.4475 ms to add 10000 elements to a List<double> with 8 reader threads.
Took 81.1967 ms to add 10000 elements to a ConcurrentBag<double> with 8 reader threads.
Took 228.7669 ms to add 10000 elements to a List<double> with 8 reader threads.
Took 164.8376 ms to add 10000 elements to a ConcurrentBag<double> with 8 reader threads.
[ ... ]
Average list time: 176.072456 ms.
Average bag time: 59.603656 ms.

So clearly it depends on exactly what you're doing with these collections.

这篇关于为什么ConcurrentBag&LT; T&GT;在.net(4.0)这么慢?我是不是做错了?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
相关文章
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆