7

Recently read about immutable collections. They are recommended to be used as a thread safe for reading, when the read operations are performed more often than write.

Then I want to test read performance ImmutableDictionary vs ConcurrentDictionary. Here is this very simple test (in .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();
        }
    }
}

You can run this code. Elapsed time in ticks will differ from time to time but the time spent by ConcurrentDictionary is several times less than by ImmutableDictionary.

This experiment makes me embarrassed. Did I do it wrong? What the reason to use immutable collections if we have concurrent? When they are preferable?

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
xneg
  • 1,204
  • 15
  • 24
  • While I can't explain your results, I can say that bench marking is not a simple thing. There are whole frameworks dedicated to ensure proper, clear and robust results. I'd recommend at least running your test's with varying configurations and number of loops. There could be a huge amount going here, especially since it involves concurrency which will always add more complexity – Dave Sep 30 '18 at 19:24
  • 1
    @Dave yes, I ran the same experiment using `BenchmarkDotNet` whith 1, 2 and 4 Threadscount. Same result - reading from `ConcurrentDictionary` is faster. – xneg Sep 30 '18 at 19:38
  • 2
    I see it. Doesn't have anything to do with a bad benchmark, concurrency or tasks. The indexer for ImmutableDictionary is just embarrassing slow, this emperor has no clothes. Consider filing a perf bug, https://github.com/dotnet/corefx/issues – Hans Passant Sep 30 '18 at 23:28
  • @HansPassant what do you think about **Akash Kava** answer? Can the indexer for tree be faster? – xneg Oct 01 '18 at 09:30
  • I think it doesn't help you. I didn't look close enough at the storage algorithm they used, if it is in fact a tree then abandon all hope. Neither Dictionary nor ConcurrentDictionary use a tree, arrays are very important for locality of reference. Your test brings out the worst of a tree design btw, dictionaries tend to get built-up in a way that makes object additions more scatter-shot and therefore lose some of the advantages of an array. I also tried making the key a string, but it is still x3 slower. Use what you learned from this test, it is accurate. – Hans Passant Oct 01 '18 at 09:47

4 Answers4

5

Immutable collections are not alternative to concurrent collections. And the way they are designed to reduce memory consumption, they are bound to be slower, the trade off here is to use less memory and thus by using less n operations to do anything.

We usually copy collections to other collections to achieve immutability to persist state. Lets see what it means,

 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...

Notice that, s2 always has only one item. Even if all items are removed.

The way all data is stored internally is a huge tree and your collection is pointing to a branch which has descendants that represents initial state of the tree.

I don't think the performance can be matched with concurrent collection where goals are totally different.

In concurrent collection, you have a single copy of collection accessed by all threads.

In immutable collection you have virtually isolated copy of a tree, navigating that tree is always costly.

It is useful in transactional system, where if a transaction has to be rolled back, state of collection can be retained in commit points.

Akash Kava
  • 39,066
  • 20
  • 121
  • 167
  • Your mentioned that immutable is a tree. I think this is the part I missed. But then I'm more confused by the phrase from **S. Cleary Concurrency Cookbook**: _"If the updates are not constant (if they’re more rare), than ImmutableDictionary may be a better choice."_ Better choice not in performance but in keeping data immutable? – xneg Oct 01 '18 at 09:26
  • @xneg "Tree" is how internally it stores information so common nodes can share the same memory space. Performance is achieved by not doing duplicating of entire collections. At every operation, a read only copy is available as immutable copy. Performance is measured in overall program, not only enumerating it. – Akash Kava Oct 01 '18 at 11:33
  • My initial question was whether multithreading scenarios exist when immutable is better in performance than concurrent. And I chose the simplest scenario - reading. As I understand from your and other answers there are no such scenarios. – xneg Oct 01 '18 at 13:25
  • I am not sure it is accurate to say concurrent collections will always perform better, as always, the answer is it depends, but the primary motivation to use ImmutableCollections is not rolling back or to traverse branches of data, they are the by-products. They really shine in providing an easy way to write deterministic code in a concise way, and in many cases they might provide performance boost in doing so, When there are multiple readers and writers, we lock without immutable c and it is difficult to reason about such code, but with immutable collections, they always give o(log n) lookup – Srivathsa Harish Venkataramana Aug 28 '23 at 23:49
2

This is a criticism that's been made before.

As Akash already said, ImmutableDictionary works with an internal tree, instead of a hashset.

One aspect of this is that you can improve the performance slightly if you build the dictionary in one step instead of iteratively adding all the keys:

  immutable = concurrent.ToImmutableDictionary();

Enumerating a hashset and a balanced tree are both O(n) operations. I took the average of a few runs on a single thread for varying container size and get results consistent with that:

Graph of tick count vs collection size

I don't know why the immutable slope is 6x steeper. For now I'll just assume its doing tricky nonblocking tree stuff. I assume this class would be optimized for random stores and reads rather than enumeration.

To identify what exact scenarios ImmutableDictionary wins at, we'd need to wrap a concurrent dictionary to provide some level of immutability, and test both classes in the face of levels of read/write contention.

Not a serious suggestion, but a counterpoint to your test is to use immutability to "cheat" over multiple iterations by comparing:

        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);

This makes a quite a difference on subsequent calls to sum an unchanged collection!

Peter Wishart
  • 11,600
  • 1
  • 26
  • 45
  • I think the most improvement in your solution goes from `(obj as ImmutableDictionary).Sum(kvp => (long)kvp.Value)`. You iterate here by **values** and not retrieve them from key. – xneg Oct 01 '18 at 13:20
  • Sorry I did simplify the question code while playing with it - didn't see a massive difference. I swapped the test code from the question back in - just using `IEnumerable.Sum` was 7.23x slower vs. the concurrent version, using the original foreach + lookup is 7.76x slower. The *cheat* I mention is caching the sum for each instance of `ImmutableDictionary`; once cached it takes less than 50 ticks. – Peter Wishart Oct 01 '18 at 14:06
  • I understand your cheat ;) So I tried your solution with only one thread. But it is really synthetic simple scenario - I don't need to sum dictionary values multiple times from different threads) But thanks for the efforts. – xneg Oct 01 '18 at 14:17
2

The two are not mutually exclusive. I use both.

If your dictionary is small the read performance of ImmutableDictionary will be superior to ConcurrentDictionary as K1*Log(N) < K2 where Log(N) < K2/K1 (when the hash table overhead is worse than tree traversal).

I personally find the write semantics of the Immutable collections easier to understand than those of the concurrent collections as they tend to be more consistent, especially when dealing with AddOrUpdate() and GetOrAdd().

In practice, I find that there have many cases in which I have a good number of small (or empty) dictionaries that are more appropriate as ImmutableDictionary and some larger ones that warrant the use of ConcurrentDictionary.

Having said that, if they are small then it doesn't make much of a difference what you use. Regarding the answer of Peter Wishart, the enumeration performance of ImmutableDictionary is higher than ConcurrentDictionary (for reasonable N) because tree traversal is brutal in terms of memory latency on modern cache architectures.

Avatar
  • 71
  • 1
  • 5
1

In .NET 8 there is a new class named FrozenDictionary<TKey,TValue> that has great improvements on read. You can read more about it here:

Stian Standahl
  • 2,506
  • 4
  • 33
  • 43