当不可变集合更可取时

时间:2018-09-30 18:57:58

标签: c# .net

最近阅读了有关不可变集合的信息。 当读取操作比写入操作执行得更多时,建议将它们用作安全读取的线程。

然后我要测试读取性能ImmutableDictionary与ConcurrentDictionary。这是一个非常简单的测试(在.NET Core 2.1中):

convert(datetime, 'Dec 30 2006 12:38AM', 109)

您可以运行此代码。滴答度过的时间有时会有所不同,但是ConcurrentDictionary花费的时间比ImmutableDictionary少几倍。

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

谢谢。

2 个答案:

答案 0 :(得分:3)

不可修改的集合不能替代并发集合。而且它们被设计为减少内存消耗的方式,它们势必会变慢,这里要权衡的是使用更少的内存,从而通过使用更少的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始终只有一项。即使所有项目都已删除。

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

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

在并发收集中,所有线程都可以访问该收集的单个副本

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

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

答案 1 :(得分:2)

这是made before的批评。

如Akash所说,ImmutableDictionary使用内部树而不是哈希集。

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

  immutable = concurrent.ToImmutableDictionary();

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

Graph of tick count vs collection size

我不知道为什么不变坡度要陡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);

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