Simple Tips to improve C# ConcurrentDictionary performance

Looking to boost the performance of your C# ConcurrentDictionary? Here are practical tips that can help you write more efficient, scalable, and maintainable concurrent code. Discover common pitfalls and best practices to get the most out of your dictionaries in multi-threaded environments.

Prefer Dictionary<>

The ConcurrentDictionary class consumes more memory than the Dictionary class due to its support for thread-safe operations. While ConcurrentDictionary is essential for scenarios where multiple threads access the dictionary simultaneously, it’s best to limit its usage to avoid excessive memory consumption. If your application does not require thread safety, opt for Dictionary instead—it’s more memory-efficient and generally faster for single-threaded scenarios.

Use GetOrAdd

Minimize unnecessary dictionary operations. For instance, if you’re adding items and don’t need to check for their existence, use TryAdd rather than Add. TryAdd skips existence checks, making bulk additions more efficient. To prevent adding duplicate items, use GetOrAdd, and for removals, TryRemove avoids pre-checking for item existence.

if (!_concurrentDictionary.TryGetValue(cachedInstanceId, out _privateClass))
{
    _privateClass = new PrivateClass();
    _concurrentDictionary.TryAdd(cachedInstanceId, _privateClass);
}

The code above misses the advantages of ConcurrentDictionary. The recommended approach is:

_privateClass = _concurrentDictionary.GetOrAdd(new PrivateClass());

Set ConcurrencyLevel

By default, ConcurrentDictionary uses a concurrency level of four times the number of CPU cores, which may be excessive and impact performance, especially in cloud environments with variable core counts. Consider specifying a lower concurrency level to optimize resource usage.

// Create a concurrent dictionary with a concurrency level of 2
var dictionary = new ConcurrentDictionary<string, int>(2);

Keys and Values are Expensive

Accessing ConcurrentDictionary.Keys and .Values is costly because these operations acquire locks and construct new list objects. Instead, enumerate KeyValuePair entries directly for better performance.

// Create a concurrent dictionary with some initial data
var dictionary = new ConcurrentDictionary<string, int>
{
    { "key1", 1 },
    { "key2", 2 },
    { "key3", 3 },
};

// Get the keys from the dictionary using the KeyValuePairs property and the ToArray method
string[] keys = dictionary.KeyValuePairs.ToArray(pair => pair.Key);

// Get the values from the dictionary using the KeyValuePairs property and the ToArray method
int[] values = dictionary.KeyValuePairs.ToArray(pair => pair.Value);

Use ContainsKey Before Lock Operations

if (this._concurrentDictionary.TryRemove(itemKey, out value))
{
    // some operations
}

Adding a non-thread-safe ContainsKey check before removal can significantly improve performance:

if (this._concurrentDictionary.ContainsKey(itemKey))
{
    if (this._concurrentDictionary.TryRemove(itemKey, out value))
    {
        // some operations
    }
}

Avoid ConcurrentDictionary.Count

The Count property in ConcurrentDictionary is expensive. For a lock-free count, wrap your dictionary and use Interlocked.Increment for atomic updates. This is ideal for tracking items or connections in a thread-safe manner.

public class Counter
{
    private int count = 0;

    public void Increment()
    {
        // Increment the count using the Interlocked.Increment method
        Interlocked.Increment(ref this.count);
        // or
        // Interlocked.Decrement(ref this.count);
    }

    public int GetCount()
    {
        return this.count;
    }
}

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *