Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C# Volatile read behavior

In the reference source code of the C#.net ConcurrentDictionary (C# reference source), I don't understand why a volatile read is required in the following code snippet:

public bool TryGetValue(TKey key, out TValue value)
{
    if (key == null) throw new ArgumentNullException("key");
      int bucketNo, lockNoUnused;

    // We must capture the m_buckets field in a local variable. 
    It is set to a new table on each table resize.
    Tables tables = m_tables;
    IEqualityComparer<TKey> comparer = tables.m_comparer;
    GetBucketAndLockNo(comparer.GetHashCode(key), 
                      out bucketNo, 
                      out lockNoUnused,
                      tables.m_buckets.Length,
                      tables.m_locks.Length);

    // We can get away w/out a lock here.
    // The Volatile.Read ensures that the load of the fields of 'n'
    //doesn't move before the load from buckets[i].
    Node n = Volatile.Read<Node>(ref tables.m_buckets[bucketNo]);

    while (n != null)
    {
        if (comparer.Equals(n.m_key, key))
        {
            value = n.m_value;
            return true;
         }
         n = n.m_next;
     }

     value = default(TValue);
     return false;
 }

The comment:

// We can get away w/out a lock here.
// The Volatile.Read ensures that the load of the fields of 'n' 
//doesn't move before the load from buckets[i].
Node n = Volatile.Read<Node>(ref tables.m_buckets[bucketNo]);

confuses me a little bit.

How can the CPU read the fields of n before reading the variable n itself from the array ?

like image 230
user1803928 Avatar asked Oct 19 '22 09:10

user1803928


1 Answers

A volatile read has acquire semantics, which means it precedes other memory accesses.

If it weren't for a volatile read, the next read to a field from the Node we just got could be reordered, speculatively, by the JIT compiler or the architecture, to before the read to the node itself.

If this doesn't make sense, imagine a JIT compiler or architecture that reads whatever value will be assigned to n, and starts to speculatively read n.m_key, such that if n != null, there's no mispredicted branch, no pipeline bubble or worse, pipeline flushing.

This is possible when the result of an instruction can be used as an operand for the next instruction(s), while yet in the pipeline.

With a volatile read or an operation with similar acquire semantics (e.g. entering a lock), both the C# specification and the CLI specification say it must occur before any further memory accesses, so it's not possible to obtain a non-initialized n.m_key.

That is, if the write was also volatile or guarded by an operation with similar release semantics (e.g. exiting a lock).

Without the volatile semantics, such a speculative read could return an uninitialized value for n.m_key.

Equally important are the memory accesses perfomed by the comparer. If the node's object was initialized without a volatile release, you could be reading stale, probably uninitialized data.

Volatile.Read is needed here because there's no way in C# itself to express a volatile read on an array element. It's not needed when reading the m_next field as it's declared volatile.

like image 54
acelent Avatar answered Oct 21 '22 05:10

acelent