When is "volatile" used instead of "lock" ?

J

Jon Skeet [C# MVP]

On Jun 22, 6:04 am, "Peter Ritchie [C#MVP]" <[email protected]>
wrote:

If the MS x86 JIT does not in fact optimize member fields to fulfil
that/those particular guarantee(s), that really just substantiates my
assertion that the spec. is unclear and that your rebuttal is intepretive.
It's also a bit contradictory to information you've said you received from
Vance and Chris. No offence or implication that you didn't receive that
information; just that it seems contradictory, if it's indeed true that the
JIT doesn't optimize member fields and therefore does not need to look for
Enter/Exit...

I don't have time to reply to everything right now (and replying to
large posts with a web browser is annoying anyway) but I've found the
information which confirms what I was saying about Vance:

http://discuss.develop.com/archives/wa.exe?A2=ind0203B&L=DOTNET&P=R375&I=-3

I mailed Vance at the time to clarify how exactly things were
guaranteed, and he came back with a reply which is *either* word for
word *or* my summary (I've just found this from a 4 year old post I
made reporting back) - bear in mind this is the previous version of
the spec, hence the numbering changes:

<quote>
Section 11.6.7 makes guarantees about volatile reads/writes,
effectively
making them memory barriers.

Section 11.6.5 (last paragraph) states that acquiring a lock
implicitly
performs a volatile read operation, and releasing a lock implicitly
performs a volatile write operation.

The two in conjunction make the appropriate guarantees.
</quote>

Jon
 
P

Peter Ritchie [C#MVP]

That goes to how an MS JIT in .NET 1.x may have been implemented. An
obscure post by Vance does nothing to clarify the spec for everyone else,
including those writing JITs on other platforms or for other processors.
I'd comment more on what Vance posted; but we're talking about the spec, not
what was implemented--it clearly doesn't follow the spec to the letter
already (good or bad).

Jon Skeet said:
On Jun 22, 6:04 am, "Peter Ritchie [C#MVP]" <[email protected]>
wrote:

If the MS x86 JIT does not in fact optimize member fields to fulfil
that/those particular guarantee(s), that really just substantiates my
assertion that the spec. is unclear and that your rebuttal is
intepretive.
It's also a bit contradictory to information you've said you received
from
Vance and Chris. No offence or implication that you didn't receive that
information; just that it seems contradictory, if it's indeed true that
the
JIT doesn't optimize member fields and therefore does not need to look
for
Enter/Exit...

I don't have time to reply to everything right now (and replying to
large posts with a web browser is annoying anyway) but I've found the
information which confirms what I was saying about Vance:

http://discuss.develop.com/archives/wa.exe?A2=ind0203B&L=DOTNET&P=R375&I=-3

I mailed Vance at the time to clarify how exactly things were
guaranteed, and he came back with a reply which is *either* word for
word *or* my summary (I've just found this from a 4 year old post I
made reporting back) - bear in mind this is the previous version of
the spec, hence the numbering changes:

<quote>
Section 11.6.7 makes guarantees about volatile reads/writes,
effectively
making them memory barriers.

Section 11.6.5 (last paragraph) states that acquiring a lock
implicitly
performs a volatile read operation, and releasing a lock implicitly
performs a volatile write operation.

The two in conjunction make the appropriate guarantees.
</quote>

Jon
 
W

Willy Denoyette [MVP]

Peter Ritchie said:
That goes to how an MS JIT in .NET 1.x may have been implemented. An
obscure post by Vance does nothing to clarify the spec for everyone else,
including those writing JITs on other platforms or for other processors.
I'd comment more on what Vance posted; but we're talking about the spec,
not what was implemented--it clearly doesn't follow the spec to the letter
already (good or bad).

True, the CLR V2 and JIT compilers (MS, Mono) do not strictly follow the
ECMA specs "memory model", the V2 CLR is currently based on a redesigned
memory model, and it's the first (and sole?) implementation of the CLI that
targets X64 and IA64, The ECMA specs. and V1.X memory model, who's target
was X86, was considered "too weak, to be used as a viable platform to write
reliable code" for a memory model weaker than X86.
Check C. Brummes's Blog post here:
http://blogs.msdn.com/cbrumme/archive/2003/05/17/51445.aspx, for some more
details on the rationale of this change.

Willy.
 
B

Ben Voigt [C++ MVP]

"Conforming implementations of the CLI are free to execute programs using
any technology that guarantees, within a single thread of execution, that
side-effects and exceptions generated by a thread are visible in the order
specified by the CIL. For this purpose only volatile operations (including
volatile reads) constitute visible side-effects."

That last sentence is pretty clear. Frequent use of volatile is needed for
locking to work properly. Maybe somewhere it is stated that all member
accesses are considered volatile (that's the behavior this thread has
observed, that member field access isn't subject to any optimization)?
 
J

Jon Skeet [C# MVP]

That last sentence is pretty clear. Frequent use of volatile is needed for
locking to work properly. Maybe somewhere it is stated that all member
accesses are considered volatile (that's the behavior this thread has
observed, that member field access isn't subject to any optimization)?

You certainly need volatile reads and writes - but that doesn't mean
you need to specify that fields are volatile, as calls to
Monitor.Enter/Exit act as volatile reads and writes.

Jon
 
W

Willy Denoyette [MVP]

Jon Skeet said:
You certainly need volatile reads and writes - but that doesn't mean
you need to specify that fields are volatile, as calls to
Monitor.Enter/Exit act as volatile reads and writes.

Jon


Next to Monitor.Enter & Exit, the framework also provides VolatileRead,
VolatileWrite and MemoryBarrier, without all these "volatile operations", it
would be nearly impossible to write "Sequential Consistent" code in VB, as
it lacks the "volatile" modifier.

Willy.
 
B

Ben Voigt [C++ MVP]

Jon Skeet said:
You certainly need volatile reads and writes - but that doesn't mean
you need to specify that fields are volatile, as calls to
Monitor.Enter/Exit act as volatile reads and writes.

But acquire and release semantics (which Monitor.Enter/Exit exhibit) only
guarantee the order of "visible side-effects" and according to the sentence
quoted, have *no effect* on the ordering of non-volatile operations.
 
J

Jon Skeet [C# MVP]

Ben Voigt said:
But acquire and release semantics (which Monitor.Enter/Exit exhibit) only
guarantee the order of "visible side-effects" and according to the sentence
quoted, have *no effect* on the ordering of non-volatile operations.

Ah, I see what you're getting at now. Interesting. The very next
sentence in the spec is interesting though:

<quote>
(Note that while only volatile operations constitute visible side-
effects, volatile operations also affect the visibility of non-volatile
references.)
</quote>

The rationale part is also interesting:

<quote>
[Rationale: An optimizing compiler is free to reorder side-effects and
synchronous exceptions to the extent that this reordering does not
change any observable program behavior. end rationale]
</quote>

I don't think this is meant to take away from the guarantees made in
12.6.7 about not reordering references to memory around volatile
operations - I *think* it's just meant to give a bit more leeway so
that if you have four operations, the first and last of which are
volatile but the middle two of which aren't, the middle two *can* be
reordered.

My annotated copy of the spec is at work, unfortunately - I should have
looked at it ages ago in this thread to see if it's any use.

As Willy said though, without these guarantees it does make multi-
threaded programming a bit of a joke. Even in C# which *does* have the
volatile modifier, you can't make a "double" variable volatile. I
believe any reading of the spec which makes it impossible to
consistently fetch the most recently written value of a double is going
over the top.
 
B

Ben Voigt [C++ MVP]

As Willy said though, without these guarantees it does make multi-
threaded programming a bit of a joke. Even in C# which *does* have the

Unless there's a statement somewhere that access to member fields is always
volatile... which would explain the complete lack of optimization by the
compiler in such cases.
 
J

Jon Skeet [C# MVP]

Ben Voigt said:
Unless there's a statement somewhere that access to member fields is always
volatile... which would explain the complete lack of optimization by the
compiler in such cases.

That would make the ability to make member fields volatile just as
silly though :)
 
W

Willy Denoyette [MVP]

Ben Voigt said:
Unless there's a statement somewhere that access to member fields is
always volatile... which would explain the complete lack of optimization
by the compiler in such cases.

Monitor.Enter is a downwards fence, so, no reads can move before it, which
means that the *first* read of a publically visible memory location must be
a load acquire. After the load acquire, the JIT is free to apply all
possible optimizations to the value read, respecting the rules as imposed by
the memory model.
Monitor.Exit is an upwards fence, that means no writes can move after it, so
the last write to a publically visible memory location must be a store
release.


Willy.
 
B

Ben Voigt [C++ MVP]

Monitor.Enter is a downwards fence, so, no reads can move before it, which
means that the *first* read of a publically visible memory location must
be a load acquire. After the load acquire, the JIT is free to apply all
possible optimizations to the value read, respecting the rules as imposed
by the memory model.
Monitor.Exit is an upwards fence, that means no writes can move after it,
so the last write to a publically visible memory location must be a store
release.

The only way that can work, is if *every* (non-inlined) method call is
treated as a memory barrier, in case it results in a volatile operation or
call to Monitor.Enter/Exit at some point.

Or else you're confusing the issue with this "publicly visible memory
location" terminology. The quote Peter found indicates "only volatile
operations constitute visible side-effects". That would mean that only
volatile operations are prevented from moving past the Monitor.Enter/Exit
fences.
 
J

Jon Skeet [C# MVP]

Ben Voigt said:
The only way that can work, is if *every* (non-inlined) method call is
treated as a memory barrier, in case it results in a volatile operation or
call to Monitor.Enter/Exit at some point.

Yup. Note that that argument is valid regardless of the behaviour of
Monitor.Enter/Exit, because of the possibility of a volatile operation
within the method.
Or else you're confusing the issue with this "publicly visible memory
location" terminology. The quote Peter found indicates "only volatile
operations constitute visible side-effects". That would mean that only
volatile operations are prevented from moving past the Monitor.Enter/Exit
fences.

It would mean that if it weren't for the other clause which talks about
not moving references past volatile operations. I don't see that clause
12.6.4 can overrule other clauses. For instance, it wouldn't allow you
to use a "technology" which didn't bother to call methods which were
known not to include volatile operations (but might output to the
screen, for instance). Taken entirely in isolation, the start of 12.6.4
sounds like you can do *anything* so long as you don't interfere with
volatile operations and exceptions. So any operations which just change
the values of non-volatile variables can be optimised away completely,
right? No, of course not - that would make a mockery of the whole
framework.

Again, the rationale provides the key to this clause: side-effects and
synchronous exceptions can be reordered so long as that reordering
doesn't change any observable program behaviour.
 
B

Ben Voigt [C++ MVP]

Jon Skeet said:
Yup. Note that that argument is valid regardless of the behaviour of
Monitor.Enter/Exit, because of the possibility of a volatile operation
within the method.


It would mean that if it weren't for the other clause which talks about
not moving references past volatile operations. I don't see that clause
12.6.4 can overrule other clauses. For instance, it wouldn't allow you
to use a "technology" which didn't bother to call methods which were
known not to include volatile operations (but might output to the
screen, for instance). Taken entirely in isolation, the start of 12.6.4
sounds like you can do *anything* so long as you don't interfere with
volatile operations and exceptions. So any operations which just change
the values of non-volatile variables can be optimised away completely,
right? No, of course not - that would make a mockery of the whole
framework.

Yes, they can. Only the resulting value must be preserved, the operations
need not.

for( int i = 0; i < 5; i++ ) x += x;

can be replaced by

x <<= 5;

Notice that i is totally gone, the increment operation is totally gone, the
addition and repeated assignment is totally gone.

And if x isn't read afterwards, the whole thing can go away.

Only for volatile variables are you guaranteed to touch the variable each
time it is mentioned in the program.
 
J

Jon Skeet [C# MVP]

Ben Voigt said:
Yes, they can. Only the resulting value must be preserved, the operations
need not.

They do if it affects observable behaviour - which needn't necessarily
involve volatile variables.

To take your example:
for( int i = 0; i < 5; i++ ) x += x;

can be replaced by

x <<= 5;

True - but:

for (int i=0; i < 5; i++)
{
Console.WriteLine (i);
x += x;
}

can't be replaced by x <<= 5; even if the JIT compiler can prove that
Console.WriteLine doesn't use any volatile operations. That's the kind
of situation which I was meaning would be utterly silly.

Likewise your original code couldn't be reordered to just x++ even
though that wouldn't change any ordering of side-effects or exceptions.
I'm playing devil's advocate here - saying that if we're going to go
for an absolute "if 12.6.4 allows it, it must be okay" reading, then
life becomes ridiculous very quickly.
Notice that i is totally gone, the increment operation is totally gone, the
addition and repeated assignment is totally gone.

And if x isn't read afterwards, the whole thing can go away.

Only for volatile variables are you guaranteed to touch the variable each
time it is mentioned in the program.

Yes - unless there are intervening volatile operations between the
reads. For instance:

using System;

class Test
{
volatile int v;
int x;

void Foo()
{
int a = x;
int b = v;
int c = x;

Console.WriteLine (a+b+c);
}
}

I believe that Foo *can't* optimised as:

int a = c = x;
int b = v;

That (in my reading) would violate 12.6.7. The same would be true if
the read of v were replaced by a call to Monitor.Enter.

In theory I believe it *could* be replaced by:

int b = v;
int a = c = x;

because that only moves a non-volatile read to *after* a volatile read,
which is okay.

Do we agree so far? (As I said to Peter, it would be good to know
*exactly* which bits of the spec we're reading differently - assuming
you agree with Peter in the first place, that is.)
 
B

Ben Voigt [C++ MVP]

Yes - unless there are intervening volatile operations between the
reads. For instance:

using System;

class Test
{
volatile int v;
int x;

void Foo()
{
int a = x;
int b = v;
int c = x;

Console.WriteLine (a+b+c);
}
}

I believe that Foo *can't* optimised as:

int a = c = x;
int b = v;

That (in my reading) would violate 12.6.7. The same would be true if
the read of v were replaced by a call to Monitor.Enter.

In theory I believe it *could* be replaced by:

int b = v;
int a = c = x;

because that only moves a non-volatile read to *after* a volatile read,
which is okay.

Do we agree so far? (As I said to Peter, it would be good to know
*exactly* which bits of the spec we're reading differently - assuming
you agree with Peter in the first place, that is.)

But what about:

int a = x;
int b = v;
int c = a; // was c = x, but x is non-volatile, so another access to x is
not required, the locally cached a can be used instead?
 
J

Jon Skeet [C# MVP]

But what about:

int a = x;
int b = v;
int c = a; // was c = x, but x is non-volatile, so another access to x is
not required, the locally cached a can be used instead?

No, I don't believe that's a legal optimisation. The reference (in IL)
is to x, and so that's logically reordered the read of x to earlier
than the read of v, and therefore prohibited by 12.6.7.

I've just checked the annotated spec by the way, and there's nothing
particularly enlightening in there unfortunately.

Jon
 
B

Ben Voigt [C++ MVP]

Jon Skeet said:
No, I don't believe that's a legal optimisation. The reference (in IL)
is to x, and so that's logically reordered the read of x to earlier
than the read of v, and therefore prohibited by 12.6.7.

Maybe not legal for the JIT to perform, but legal for the language compiler,
if it can prove that the expression "x" doesn't depend on the value of b?
 
J

Jon Skeet [C# MVP]

Maybe not legal for the JIT to perform, but legal for the language compiler,
if it can prove that the expression "x" doesn't depend on the value of b?

The language spec is sadly silent on this front as far as I can see.
However, I think for the purposes of this discussion we should assume
for the moment that the IL represents the C# code in the most obvious
way.

That's not to say it's not a potential issue, just that it's worth
concentrating on one thing at a time :)

If we can all agree it's not a valid JIT (or CPU) optimisation, that
would be a good start.

Jon
 
J

Jon Skeet [C# MVP]

Jon Skeet said:
The language spec is sadly silent on this front as far as I can see.
However, I think for the purposes of this discussion we should assume
for the moment that the IL represents the C# code in the most obvious
way.

It appears I was wrong about the language spec not mentioning the
memory model.

Section 17.4.3 talks about volatile fields and release/acquire
semantics, and mentions the lock statement, but *doesn't* have the same
bit as the CLI spec in terms of specifying that acquiring a lock
performs an implicit volatile read and releasing a lock performs an
implicit volatile write.

I'll mail the C# team to see if this can be fixed.
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Top