Does volatile prevent introduced reads or writes?
Asked Answered
M

3

9

In C#, volatile keyword ensures that reads and writes have acquire and release semantics, respectively. However, does it say anything about introduced reads or writes?

For instance:

volatile Thing something;
volatile int aNumber;

void Method()
{
    // Are these lines...
    var local = something;
    if (local != null)
        local.DoThings();

    // ...guaranteed not to be transformed into these by compiler, jitter or processor?
    if (something != null)
        something.DoThings(); // <-- Second read!



    // Are these lines...
    if (aNumber == 0)
        aNumber = 1;

    // ...guaranteed not to be transformed into these by compiler, jitter or processor?
    var temp = aNumber;
    if (temp == 0)
        temp = 1;
    aNumber = temp; // <-- An out-of-thin-air write!
}
Moltke answered 31/10, 2018 at 8:55 Comment(6)
I think it's fairly obvious that it wouldn't be useful as anything like C++ atomic if things like that could happen, so I think you're mostly just asking whether that's actually standardized somewhere the way volatile is in C++ (where each read/write is considered an observable side-effect that optimization must preserve), or whether useful implementations just always work that way. I don't know C#, but does ++aNumber do an atomic RMW, or is it an atomic load and a separate atomic store?Nitrile
@PeterCordes It's not atomic so I'll rephrase it a bit. The point is that the write happens outside the if block.Moltke
Thanks, so it is mostly like C++ volatile in that respect (plus other semantics that C++ doesn't have), not C++11 atomic. But yes, I see the extra write which wouldn't happen at all with the original. And also one fewer read, so the write is always storing 1 regardless of racing with other threads. Unlike with ++aNumber, where another thread could have changed aNumber between the zero-check and the read that's part of the increment. (Well, until you simplified it with your last edit).Nitrile
@PeterCordes Sorry, was a bit unclear there. ++ is not an atomic operation, but volatile is only allowed to be declared with types capable of atomic reads and writes.Moltke
IDK when you'd ever use code like that, but it seems plausible enough that you'd have a thread that unconditionally stores a value only after seeing some other value. But where a non-atomic RMW of any other value would not be ok. Non-atomic RMWs are a fairly well-known problem, so anything that introduces one that didn't exist before is obviously bad for a compiler dealing with multi-threaded code.Nitrile
The C# compiler doesn't try to optimize away e.g. local variables, so certainly the emitted IL will not contain your unwanted read transformation, and I don't think it could do anything like your write example either. Then we have to delve into what a compliant CLR can do and that takes more digging.Rubino
R
5

Here's what the C# spec1 has to say about Execution Order:

Execution of a C# program proceeds such that the side effects of each executing thread are preserved at critical execution points. A side effect is defined as a read or write of a volatile field ...

The execution environment is free to change the order of execution of a C# program, subject to the following constraints:

...

The ordering of side effects is preserved with respect to volatile reads and writes ...

I would certainly consider introducing new side effects to be changing the order of side effects, but it's not explicitly stated like that here.


Link in answer is to the C# 6 spec which is listed as Draft. C# 5 spec isn't a draft but is not available on-line, only as a download. Identical wording, so far as I can see in this section.

Rubino answered 31/10, 2018 at 9:50 Comment(1)
That spec says a side effect is "a read or write of a volatile field, a write to a non-volatile variable, a write to an external resource, and the throwing of an exception". That seems to mean my second example of an out-of-thin-air write is impossible even with a non-volatile field, right?Moltke
N
2

This wording from the C# spec:

The ordering of side effects is preserved with respect to volatile reads and writes...

may be interpreted as implying that read and write introductions on volatile variables are not allowed, but it is really ambiguous and it depends on the meaning of "ordering." If it is referring to relative ordering of existing accesses, then introducing new reads or writes does not change that and so it would not violate this part of the spec. If it is referring to the exact position of all memory accesses in program order, then introducing new accesses would violate the spec.

This article says that reads on non-volatile variables might be introduced but does not say explicitly whether this is not allowed on volatile variables.

This Q/A discusses how to prevent read introduction (but no discussion on write introduction).

In the comments under this article, two Microsoft employees (at the least at the time the comments were written) explicitly state that read and write introductions on volatile variables are not allowed.

Stephen Toub

"read introduction" is one mechanism by which a memory reordering might be introduced.

Igor Ostrovsky

Elsewhere in the C# specification, a volatile read is defined to be a "side effect". As a result, repeating the read of m_paused would be equivalent to adding another side effect, which is not allowed.

I think we can conclude from these comments that introducing a side effect out-of-thin-air in C#, any kind of side effect, anywhere in the code is not allowed.

A related quote from the CLI standard states the following in Section I.12.6.7:

An optimizing compiler that converts CIL to native code shall not remove any volatile operation, nor shall it coalesce multiple volatile operations into a single operation.

As far as I know, the CLI does not explicitly talk about introducing new side effects.

Nims answered 3/11, 2018 at 8:50 Comment(0)
S
-1

I wonder whether you have misunderstood what volatile means. Volatile can be used with types that can be read or written as an atomic action.

There is no acquire/release of a lock, only a barrier to compile-time and run-time reordering to provide lockless aquire/release semantics (https://preshing.com/20120913/acquire-and-release-semantics/). On non-x86 this may require barrier instructions in the asm, but not taking a lock.

volatile indicates that a field may be modified by other threads, which is why read/writes need to be treated as atomic and not optimised.


Your question is a little ambiguous.

1/ If you mean, will the compiler transform:

var local = something;
if (local != null) local.DoThings();

into:

if (something != null) something.DoThings();

then the answer is no.

2/ If you mean, will "DoThings()" be called twice on the same object:

var local = something;
if (local != null) local.DoThings();
if (something != null) something.DoThings(); 

then the answer is mostly yes, unless another thread has changed the value of "something" before the second "DoThings()" is invoked. If this is the case then it could give you a run time error - if after the "if" condition is evaluated and before "DoThings" is called, another thread sets "something" to null then you will get a runtime error. I assume this is why you have your "var local = something;".

3/ If you mean will the following cause two reads:

if (something != null) something.DoThings();

then yes, one read for the condition and a second read when it invokes DoThings() (assuming that something is not null). Were it not marked volatile then the compiler might manage that with a single read.

In any event the implementation of the function "DoThings()" needs to be aware that it could be called by multiple threads, so would need to consider incorporating a combination of locks and its own volatile members.

Stoicism answered 31/10, 2018 at 11:7 Comment(12)
I'm not sure where you get "no acquire/release semantics" when the spec says: “A volatile read has "acquire semantics"; ... A volatile write has "release semantics";”Rubino
Sorry, I wasn't clear. What I meant by that is that there are no additional acquire or lock instructions implicitly involved. I had wondered from the question whether there was a suggestion that an implicit lock might be introduced. You are correct, "acquire semantics" in this case meaning (as you indicated in your reply) that the read will occur prior to any memory references that occur after it in the instruction sequence. Ditto with write.Stoicism
Edited the code a bit to hopefully make it a bit more clear. I meant your option 1.Moltke
The terms "acquire" and "release" semantics refer to the fact that if a release and acquire are performed on an object in that order, all actions on that object which precede the release will complete before the commencement of any actions on that object which follow the acquire. I think the names stem from the idea that if some code does some actions while holding a lock and releases that lock, and some other code acquires the lock and does some actions, the first group of actions will be fully sequenced with regard to the second.Cobb
@BrianCryer: x86 doesn't need any additional asm instructions because x86's strongly-ordered memory model already has acquire/release semantics for loads/stores (preshing.com/20120913/acquire-and-release-semantics and preshing.com/20120930/weak-vs-strong-memory-models). (The lock prefix is only needed for atomic RMW operations, which C# volatile does not provide. See my answer on Can num++ be atomic for 'int num'? for details of how atomic RMW works in modern CPUs.)Nitrile
I know and I agree. The point I was trying to make was just that.Stoicism
But C# compiled for ARM would need extra memory barrier instructions to implement volatile, unless you're claiming that the ordering guarantees from volatile depend on the target architecture, and acq/rel is only guaranteed on x86. (C++ std::atomic_thread_fence(std::memory_order_acquire) on x86 only needs to stop compile-time reordering. But on ARM it also needs a dmb ish.)Nitrile
That is interesting to know.Stoicism
Your answer still says "There is no acquire/release." There is no acquiring of a separate lock variable, but a volatile read is still an acquire operation. Technical language is critical in an answer to a question like this.Nitrile
You are still totally misunderstanding or incorrectly describing it. The dmb ish (Data Memory Barrier) instruction on ARM (used by gcc godbolt.org/z/VrgqTZ) is a memory barrier instruction, not a lock to be taken / released. See also preshing.com/20120710/…. (DMB ISH is a full barrier that blocks all 4 kinds of reordering, including StoreLoad.) ARM can provide acq/rel semantics for lockless code.Nitrile
If you suggest some working then I'll update it - but I would like to keep the point that acquire/release may imply a lock and that any implication to that effect is incorrect.Stoicism
Ok, I tried to fix your answer. Now it just seems pointless. The OP was already correctly using the standard terminology "acquire/release semantics" to describe the specific lockless synchronization guarantees you get with x86 asm loads/stores, for example. But sure, clarify that it doesn't involve taking an actual lock if you like, even on ARM. Nobody (except you) was saying that it did. And now you're not either, but maybe some future readers will benefit or something if they stumble upon this question and don't understand the terminology.Nitrile

© 2022 - 2024 — McMap. All rights reserved.