Reputation: 3290
My colleague and I are having an argument on atomicity of reading a double on an Intel architecture using C# .NET 4.0. He is arguing that we should use Interlocked.Exchange
method for writing into a double
, but just reading the double value (in some other thread) is guaranteed to be atomic. My argument is that .NET doesn't guarantee this atomicity, but his argument is that on an Intel architecture this is guaranteed (maybe not on AMD, SPARC, etc.).
Any Intel and .NET experts share some lights on this?
Reader is OK to read a stale (previous) value, but not incorrect value (partial read before and after write giving a garbage value).
Upvotes: 31
Views: 4828
Reputation: 354
As of .Net8 we have this doc: https://github.com/dotnet/runtime/blob/main/docs/design/specs/Memory-model.md#alignment
We are interested in sections Alignment and Atomic memory accesses.
Read the link for details, but in general:
Memory accesses to properly aligned data of primitive and Enum types with sizes up to the platform pointer size are always atomic.
Upvotes: 1
Reputation: 20842
Yes and no. On 32-bit processors, it is not guaranteed atomic, because double is larger than the native wordsize. On a 64-bit processor properly aligned access is atomic. The 64-bit CLI guarantees everything up to a 64-bit read as atomic. So you'd need to build your assembly for x64 (not Any CPU). Otherwise if your assembly may be run on 32-bit, you better use Interlocked, see Atomicity, volatility and immutability are different, part two by Eric Lippert. I think you can rely on Eric Lippert's knowledge of the Microsoft CLR.
The ECMA CLI standard also supports this, even though C# itself does not guarantee it:
A conforming CLI shall guarantee that read and write access to properly aligned memory locations no larger than the native word size (the size of type native int) is atomic (see §I.12.6.2)
It also says that on processors where operations are atomic, Interlocked methods are often compiled to a single instruction, so in my book there is no performance penalty in using it. On the other hand, there may be a worse penalty to not using it when you should.
Another related Stack Overflow question is What operations are atomic in C#?.
Upvotes: 20
Reputation: 660038
My colleague and I are having an argument on atomicity of reading a double on an Intel architecture using C# .NET 4.0.
Intel guarantees that 8 byte doubles are atomic on read and write when aligned to an 8 byte boundary.
C# does not guarantee that a double is aligned to an 8 byte boundary.
He is arguing that we should use
Interlocked.Exchange
method for writing into a double, but just reading the double value (in some other thread) is guaranteed to be atomic.
Your colleague is not thinking this through carefully. Interlocked operations are only atomic with respect to other interlocked operations. It doesn't make any sense to use interlocked operations some of the time; this is like saying that traffic that is going through the intersection to the north doesn't have to obey the traffic light because traffic that is going through the intersection to the west does obey the traffic light. Everyone has to obey the lights in order to avoid collisions; you can't do just half.
My argument is that .NET doesn't guarantee this atomicity, but his argument is that on an Intel architecture this is guaranteed (maybe not on AMD, SPARC, etc.).
Look, suppose that argument were correct, which it isn't. Is the conclusion that we're supposed to reach here is that the several nanoseconds that is saved by doing it wrong are somehow worth the risk? Forget about interlocked. Take a full lock every time. The only time you should not take a full lock when sharing memory across threads is when you have a demonstrated performance problem that is actually due to the twelve nanosecond overhead of the lock. That is when a twelve nanosecond penalty is the slowest thing in your program and that is still unacceptable, that's the day you should consider using a low-lock solution. Is the slowest thing in your program taking a 12 nanosecond uncontended lock? No? Then stop having this argument, and spend your valuable time making the parts of your program that take more than 12 nanoseconds faster.
Reader is OK to read a stale (previous) value, but not incorrect value (partial read before and after write giving a garbage value).
Don't conflate atomicity with volatility.
The interlocked operations, and the lock statement, will both make a memory barrier that ensures that the up-to-date value is read or published. An ordinary non-volatile read or write is not required to do so; if it happens to do so, you got lucky.
If these sorts of issues interest you, a related issue that I am occasionally asked about is under what circumstances a lock around an integer access can be elided. My articles on that subject are:
Upvotes: 23
Reputation: 69260
Reading or writing a double is atomic on Intel architecture if they are aligned on an 8-byte address boundary. See Is Updating double operation atomic.
Even though reads and writes of doubles might effectively be atomic in .NET code on Intel architecture, I wouldn't trust it as the C# spec doesn't guarantee it, see this quote from an Answer by Eric Lippert.
Reads and writes of the following data types are atomic: bool, char, byte, sbyte, short, ushort, uint, int, float, and reference types. In addition, reads and writes of enum types with an underlying type in the previous list are also atomic. Reads and writes of other types, including long, ulong, double, and decimal, as well as user-defined types, are not guaranteed to be atomic.
Use Interlocked
for reading and writing to be safe. It guarantees atomicity. On an architecture where it is atomic by default, it shouldn't produce any overhead. You need to use Interlocked
for reading as well writing to ensure that no partially written values are read (quote from InterLocked.Read()
documentation):
The Read method and the 64-bit overloads of the Increment, Decrement, and Add methods are truly atomic only on systems where a System.IntPtr is 64 bits long. On other systems, these methods are atomic with respect to each other, but not with respect to other means of accessing the data. Thus, to be thread safe on 32-bit systems, any access to a 64-bit value must be made through the members of the Interlocked class.
Upvotes: 18