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 ?
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
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With