【问题标题】:When immutable collections are preferable than concurrent当不可变集合比并发更可取时
【发布时间】:2018-09-30 18:57:58
【问题描述】:

最近阅读了关于不可变集合的内容。 当读取操作的执行频率高于写入操作时,建议将它们用作读取的线程安全。

然后我想测试读取性能ImmutableDictionaryConcurrentDictionary。这是这个非常简单的测试(在 .NET Core 2.1 中):

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;

namespace ImmutableSpeedTests
{
    class Program
    {
        public class ConcurrentVsImmutable
        {
            public int ValuesCount;
            public int ThreadsCount;

            private ImmutableDictionary<int, int> immutable = ImmutableDictionary<int, int>.Empty;
            private ConcurrentDictionary<int, int> concurrent = new ConcurrentDictionary<int, int>();

            public ConcurrentVsImmutable(int valuesCount, int threadsCount)
            {
                ValuesCount = valuesCount;
                ThreadsCount = threadsCount;
            }

            public void Setup()
            {
                // fill both collections. I don't measure time cause immutable is filling much slower obviously.
                for (var i = 0; i < ValuesCount; i++)
                {
                    concurrent[i] = i;
                    immutable = immutable.Add(i, i);
                }
            }

            public async Task<long> ImmutableSum() => await Sum(immutable);

            public async Task<long> ConcurrentSum() => await Sum(concurrent);

            private async Task<long> Sum(IReadOnlyDictionary<int, int> dic)
            {
                var tasks = new List<Task<long>>();

                // main job. Run multiple tasks to sum all values.
                for (var i = 0; i < ThreadsCount; i++)
                    tasks.Add(Task.Run(() =>
                    {
                        long x = 0;
                        foreach (var key in dic.Keys)
                        {
                            x += dic[key];
                        }
                        return x;
                    }));

                var result = await Task.WhenAll(tasks.ToArray());
                return result.Sum();
            }
        }

        static void Main(string[] args)
        {
            var test = new ConcurrentVsImmutable(1000000, 4);

            test.Setup();

            var sw = new Stopwatch();

            sw.Start();
            var result = test.ConcurrentSum().Result;
            sw.Stop();

            // Convince that the result of the work is the same
            Console.WriteLine($"Concurrent. Result: {result}. Elapsed: {sw.ElapsedTicks}.");

            sw.Reset();
            sw.Start();
            result = test.ImmutableSum().Result;
            sw.Stop();

            Console.WriteLine($" Immutable. Result: {result}. Elapsed: {sw.ElapsedTicks}.");

            Console.ReadLine();
        }
    }
}

您可以运行此代码。以滴答为单位的经过时间会不时变化,但ConcurrentDictionary 花费的时间比ImmutableDictionary 少几倍。

这个实验让我很尴尬。我做错了吗?如果我们有并发,使用不可变集合的原因是什么?什么时候更可取?

【问题讨论】:

  • 虽然我无法解释你的结果,但我可以说基准测试并不是一件简单的事情。有完整的框架专门用于确保正确、清晰和稳健的结果。我建议至少使用不同的配置和循环数运行您的测试。这里可能会有大量的数据,尤其是因为它涉及到并发性,这总是会增加更多的复杂性
  • @Dave 是的,我使用BenchmarkDotNet 进行了相同的实验,线程数为 1、2 和 4。结果相同 - 从ConcurrentDictionary 读取更快。
  • 我明白了。与糟糕的基准测试、并发性或任务无关。 ImmutableDictionary 的索引器只是慢的尴尬,这个皇帝没有衣服。考虑提交性能错误,github.com/dotnet/corefx/issues
  • @HansPassant 您如何看待 Akash Kava 的答案?树的索引器可以更快吗?
  • 我认为它对你没有帮助。我没有仔细看他们使用的存储算法,如果它实际上是一棵树,那就放弃所有希望。 Dictionary 和 ConcurrentDictionary 都不使用树,数组对于引用的局部性非常重要。顺便说一句,您的测试带来了树设计的最糟糕之处,字典往往以一种使对象添加更加分散的方式建立起来,因此失去了数组的一些优势。我还尝试将密钥设为字符串,但它仍然慢 3 倍。使用你从这次测试中学到的东西,它是准确的。

标签: c# .net concurrency concurrent-collections immutable-collections


【解决方案1】:

不可变集合不能替代并发集合。而且它们旨在减少内存消耗的方式,它们必然会更慢,这里的权衡是使用更少的内存,从而使用更少的 n 操作来做任何事情。

我们通常将集合复制到其他集合以实现不变性以保持状态。让我们看看它是什么意思,

 var s1 = ImmutableStack<int>.Empty;
 var s2 = s1.Push(1);
 // s2 = [1]

 var s3 = s2.Push(2);
 // s2 = [1]
 // s3 = [1,2]

 // notice that s2 has only one item, it is not modified..

 var s4 = s3.Pop(ref var i);

 // s2 = [1];
 // still s2 has one item...

请注意,s2 始终只有一项。即使所有项目都被删除。

所有数据在内部存储的方式是一棵巨大的树,您的集合指向一个分支,该分支具有代表树初始状态的后代。

我认为性能无法与目标完全不同的并发收集相匹配。

在并发收集中,所有线程都可以访问一个集合副本

在不可变集合中,您实际上拥有一棵树的独立副本,导航该树总是很昂贵

在事务系统中很有用,如果事务必须回滚,集合状态可以保留在提交点中。

【讨论】:

  • 您提到不可变是一棵树。我想这是我错过的部分。但是后来我对 S 中的短语更加困惑。 Cleary Concurrency Cookbook“如果更新不是恒定的(如果它们更罕见),那么 ImmutableDictionary 可能是更好的选择。” 更好的选择不是性能但要保持数据不可变?
  • @xneg “树”是它在内部存储信息的方式,因此公共节点可以共享相同的内存空间。通过不复制整个集合来实现性能。在每个操作中,只读副本都可用作不可变副本。性能是在整个程序中衡量的,而不仅仅是枚举它。
  • 我最初的问题是当不可变的性能优于并发时是否存在多线程场景。而我选择了最简单的场景——阅读。正如我从您和其他答案中了解到的那样,没有这种情况。
【解决方案2】:

这是madebefore的批评。

正如 Akash 已经说过的,ImmutableDictionary 使用内部树,而不是哈希集。

其中一个方面是,如果您一步构建字典而不是迭代地添加所有键,则可以稍微提高性能:

  immutable = concurrent.ToImmutableDictionary();

枚举散列集和平衡树都是O(n) 操作。对于不同的容器大小,我在单个线程上平均运行了几次,得到的结果与此一致:

我不知道为什么不可变斜率要陡峭 6 倍。现在我只假设它在做棘手的非阻塞树的事情。我假设这个类将针对随机存储和读取而不是枚举进行优化。

要确定ImmutableDictionary 胜出的确切场景,我们需要包装一个并发字典以提供某种程度的不变性,并在面对读/写争用级别时测试这两个类。

不是一个严肃的建议,但与您的测试相反的是,通过比较使用不变性来“欺骗”多次迭代

        private ConcurrentDictionary<object, long> cache = new ConcurrentDictionary<object, long>();
        public long ImmutableSum() 
        {
            return cache.GetOrAdd(immutable, (obj) => (obj as ImmutableDictionary<int, int>).Sum(kvp => (long)kvp.Value));                
        }

        public long ConcurrentSum() => concurrent.Sum(kvp => (long)kvp.Value);

这对后续调用对未更改集合求和的方法有很大的不同!

【讨论】:

  • 我认为您的解决方案中最大的改进来自(obj as ImmutableDictionary&lt;int, int&gt;).Sum(kvp =&gt; (long)kvp.Value)。您在这里通过 values 进行迭代,而不是从 key 中检索它们。
  • 对不起,我在玩它的时候确实简化了问题代码 - 没有看到很大的不同。我将问题中的测试代码换回了 - 仅使用 IEnumerable.Sum 比并发版本慢 7.23 倍,使用原始 foreach + 查找慢 7.76 倍。我提到的 cheat 是缓存ImmutableDictionary 的每个实例的总和;一旦缓存,它需要少于 50 个滴答声。
  • 我理解你的作弊 ;) 所以我只用一个线程尝试了你的解决方案。但这确实是一种综合的简单场景——我不需要从不同的线程多次对字典值求和)但是感谢您的努力。
【解决方案3】:

两者并不相互排斥。我两个都用。

如果您的字典很小,则 ImmutableDictionary 的读取性能将优于 ConcurrentDictionary,因为 K1*Log(N)

我个人发现不可变集合的写语义比并发集合的更容易理解,因为它们往往更加一致,尤其是在处理 AddOrUpdate() 和 GetOrAdd() 时。

在实践中,我发现在很多情况下,我有大量更适合作为 ImmutableDictionary 的小型(或空)字典,以及一些需要使用 ConcurrentDictionary 的大型字典。

话虽如此,如果它们很小,那么您使用的东西并没有太大的区别。 关于 Peter Wishart 的回答,ImmutableDictionary 的枚举性能高于 ConcurrentDictionary(对于合理的 N),因为在现代缓存架构上,就内存延迟而言,树遍历是残酷的。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2017-05-21
    • 1970-01-01
    • 2012-02-12
    • 2012-01-19
    • 2011-03-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多