Peter Ritchie said:
Lack of a clause does not make the opposite a spec. requirement.
But there's already a clause defining the behaviour - 12.6.7. Why are
you assuming it only applies to *part* of the behaviour rather than the
overall system behaviour?
And why shouldn't it address the two separately?
It *could* do, but I don't see that it does.
But, the spec *does* distinguish the two
Where? In particular, where *exactly* does it state that clause 12.6.7
only applies to the JIT, or only applies to the CPU? Surely unless
there is something to explicitly abdicate responsibility from one area,
clauses should apply to overall system behaviour - otherwise the spec
has no value.
The whole 12.6.4 section is about optimization and it only
describes single thread of execution guarantees, and makes no mention of
"semantics" ("acquire" or "release"). Everything else you quote is outside
of 12.6.4 and is always in the context of "acquire semantics" or "release
semantics" (ergo in the context of processor caching). The other sections in
12.6 relevant to our conversation deal with locking (and its relationship to
volatile operations) and how volatile operations affect the processor's
cache. 12.6.4 doesn't come out and say the "JIT compiler" but it does say
"the CLI", which can't mean the C#-to-IL compiler and without a JIT we can't
get from IL to native instructions.
But the CLI's system behaviour is surely defined by the combination of
the JIT *and* the CPU. Put it this way: on a CPU which didn't provide
any atomicity guarantees itself, I wouldn't expect the CLR to say "Oh,
never mind - the clause on atomic reads and writes (12.6.6) just
doesn't apply". The implementation would have to work round the issue
somehow, to make the overall system behaviour comply with the spec.
How can it be irrelevant?
I'm discussing what is documented as "compliancy"
requirements for that JIT. I agree it needs to be in the spec. and am
willing to view "the CLI" as "The JIT" for the purposes of what is required
of the JIT.
*You're* willing to view it that way - but *I'm* only willing to view
it as overall system behaviour.
In other words, suppose there were two CPU architectures which were
identical except for the guarantees that their own memory models made.
I'm saying that a CLI implementation whose JIT generated the same code
for both architectures could well be compliant on one architecture and
not compliant on the other. Do you disagree with that? If not, how can
the CPU be irrelevant?
Yes, not to the CLR/CLI. But with regard to C#, reusing the keyword
"volatile" in C# pulls that baggage in. If "volatile" in C# wasn't intended
to be the same as "volatile" in C++ it should have been named differently.
It's *not* the same in C# as it is in C++. It's simply not - and I
don't see any reason to believe they *are* the same.
As another example of this, consider "char" - does that mean the same
in C# and in C++? Certainly not - and any reading of the specification
which assumed they were the same would be incorrect.
If the goal was to ease compilation of C++ code, then they've done
programmers a hell of a disservice. It also goes to MT issues outside of
.NET 2.0 and that the issues of compiler optimizations exist and can be
dealt with (although much more difficultly) in languages that support
volatility. Some language don't, yes, and they fail miserably at MT; but
that's a separate issue. And I'll admit it's anecdotal to my point; just as
your external references are.
Which external references do you think I'm relying on, other than how
other people (including the people who really should know!) read the
spec we're discussing?
It also brings in the issue that use of "volatile" for variables despite
always being synchronized is common-place and shows separation of volatility
and synchronization, and shouldn't be considered abhorrent in .NET 2.0.
Other than your lock protocol of
only-access-that-member-within-a-lock-block-locked-on-the-same-object,
volatility must be addressed through the "volatile" (or always use
Thread.Volatile* or Threading.Interlocked.*, which I don't recommend unless
"volatile" doesn't apply) in .NET 2.0.
Other than the lock protocol, I agree - but I don't see any reason to
abandon the lock protocol, nor do I believe you've presented any
evidence to suggest it isn't guaranteed to work according to the spec.
If you do, most of the statements in 12.6 are contradictory and nonsensical
and no more useful. You can't seriously tell me that reference to *any*
unrelated memory is guaranteed to execute before a virtual operation?
Define "virtual operation". If you mean something like "invocation of a
virtual member" then I'd certainly expect any writes occurring in the
IL stream before the invocation to be guaranteed to occur before the
operation, and any reads after the invocation to be guaranteed to occur
after the operation. After all, that's what the spec says.
Clearly, and makes sense if, that's discussing processor caching and not
optimization restrictions.
Not sure what you mean here, but I suspect I disagre...
e.g.
void Method() {
this.memberNumber = 10;
lock(this.syncObject)
{
String temp = this.memberString;
this.memberString = "Frank";
//...
}
}
Who cares if the processor instruction to assign 10 to MemberNumber is
executed before or after the lock?
If it's executed after the lock is released, then another thread which
acquires the lock could see the write to memberString but not the write
to memberNumber, contrary to the spec.
Dealing strictly with processor caching
in 12.6.7 para 2, of course if the processor cache is flushed at a virtual
operation it "...is guaranteed to occur prior to any references to memory
that occur after the [operation's] instruction in the CIL instruction
sequence." and any cached writes to this.memberString are flushed before
it's value is assigned to "temp". Notice there's nothing about when an
instruction is executed in 12.6.7 para 2.
I don't see your point, I'm afraid. As far as I can see, 12.6.7 is
still guaranteeing that the locking protocol will work.
Agreed, and neither does any other external reference.
I don't believe any external reference is really needed for the spec to
guarantee locking. However, I believe it's helpful to see how other
people interpret the same spec, but *not* helpful to bring in specs for
different platforms.
I agree we disagree (I over generalized with "12.6" I should have said
12.6.4-12.6.8 inclusive, the rest of 12.6 doesn't apply to what we're
talking about), but you haven't referenced anything in the spec that
definitively backs up your opinion.
So you'd want to see another clause that says, "By the way, we really
did mean clause 12.6.7, just in case you were wondering"? I don't see
why. Clause 12.6.7 guarantees that in an instruction sequence of:
Volatile read
Memory access
Memory access
Memory access
Volatile write
the three memory accesses can be reordered between the volatile
operations without violation of 12.6.7 (subject to other clauses) but
none of the memory accesses can effectively occur before the volatile
read or after the volatile write.
That, along with "acquiring a lock counts as a volatile read" and the
equivalent for releasing a lock, is all that's required for the locking
protocol to be guaranteed to work.
Whereas I believe I've clearly shown
that the 12.6 is only clear and unambiguous if you separate optimizations
from processor caching (and define "acquire semantics" and "release
semantics" as applying only to processor caching) and say that 12.6.4 is the
only section affecting JIT optimizations.
I see no evidence to suggest that the whole section shouldn't be
applied to the system as a whole. Without that, the spec is worthless.
I agree there are pitfalls to MT programming with my view of the spec, but
no more than any other framework. Your view doesn't eliminate pitfalls to
MT programming in .NET 2.0. My view simply means you must consider
volatility, and separately from synchronization and declare members accessed
by multiple threads with "volatile" or with "Volatile{Read|Write}", just
like most other languages with optimizing compilers.
My view certainly doesn't eliminate pitfalls in MT programming - but it
has a relatively straightforward protocol (the locking protocol) which
is guaranteed (IMO) to work with *all* data types.
You still haven't explained (as far as I can remember) how you deal
with variables which can't be declared volatile, such as variables of
type "double". Do you believe that the spec provides no way of coping
with such data in a thread-safe manner?
And I disagree 335 is making that implication and that it can be viewed as
absolute with your interpretation; otherwise you have to interpret 12.2.7
para 2 as not really meaning *all* references to memory, with regard to CIL
instructions, from "any references memory", for example.
Why?
12.6.7 makes no mention of not reordering instructions and is completely
unambiguous if it's viewed only in the context of processor caching.
It's unambiguous when viewed in the context of the memory model itself,
without regard to the details of CPUs.
Yes, I have, and it's a wonderful read on the Microsoft 2.0 .NET
implementation.
Are we reading the same page? This is the one I was referring to:
http://msdn.microsoft.com/msdnmag/issues/05/10/MemoryModels/
The sections about the locking protocol include the following:
<quote>
Clearly, the ability to arbitrarily move memory accesses anywhere at
all would lead to chaos, so all practical memory models have the
following three fundamental rules
[snip rules]
</quote>
In what way can that be said to only apply to the .NET 2.0 memory
model? Likewise, here's another part of the page:
<quote>
This model is very efficient, but requires that programs follow the
locking protocol or explicitly mark volatile accesses when using low-
lock techniques.
</quote>
That quote is taken directly from the section entitled: "A Relaxed
Model: ECMA". Again, how can that be deemed to apply only to the .NET
2.0 memory model?
Note the "or" in the quote, which clearly implies that using the
locking protocol is a viable alternative to explicitly marking volatile
accesses.
If that were in the spec we wouldn't be having this
conversation, not that I believe Vance's article is normatively worded.
The rules that the locking protocol relies on *are* in the spec.
Your assertions are about guarantees in 335. If you have to refer to
external documents then you're basically agreeing with me that 335 isn't
clear with respect to JIT optimizations (or there aren't cross-threading
restrictions on JIT optimizations and your locking protocol requires the
addition of "volatile" or "Thread.Volatile{Read|Write}").
My only reason for referring to the external documents was to say,
"Hey, here are people who should really understand the ECMA spec, given
that they were either closely involved in writing it, or know the
people who actually wrote it."
In other words, they're in a very good position to understand what the
spec is really saying.
Even Joe Duffy describes Vance's article as a "reference on the 2.0 memory
model, as implemented".
It's that as well, certainly - but the section on the locking protocol
comes before any of the .NET 2.0-specific parts.
If all you're writing for are Windows platforms,
you can be reasonably comfortable that your locking protocol is backed up by
Vance's description; but you can't say there are spec guarantees for it.
I *can* say there are spec guarantees for it, because as far as I can
see 12.6 provides all the rules that are required.