Allowed C# Compiler optimization on local variables and refetching value from memory
EDIT: I am asking what happens when two threads concurrently access the same data without proper synchronization (before this edit, that point was not expressed clearly).
I have a question about the optimizations that are performed by the C# compiler and by the JIT compiler.
Consider the following simplified example:
class Example {
private Action _action;
private void InvokeAction() {
var local = this._action;
if (local != null) {
local();
}
}
}
Please ignore in the example that reading _action
might yield a cached and outdated value as there is no volatile specifier nor any other sychronization. That is not the point :)
Is the compiler (or actually the jitter at runtime) allowed to optimize the assignment to the local variable out and instead reading _action
from memory twice:
class Example {
private Action _action;
private void InvokeAction() {
if (this._action != null) {
this._action(); // might be set to null by an other thread.
}
}
}
which might throw a NullReferenceException
when the field _action
is set to null
by a concurrent as开发者_运维知识库signment.
Of course in this example such an "optimization" would not make any sense because it would be faster to store the value in a register and thus using the local variable. But in more complex cases, is there a guarantee that this works as expected without re-reading the value from memory?
I will say (partially) the opposite of mgronber :-) Aaaah... In the end I'm saying the same things... Only that I'm quoting an article :-( I'll give him a +1.
It's a LEGAL optimization under the ECMA specifications, but it's an illegal optimization under the .NET >= 2.0 "specifications".
From the Understand the Impact of Low-Lock Techniques in Multithreaded Apps
Read here Strong Model 2: .NET Framework 2.0
Point 2:
Reads and writes cannot be introduced.
The explanation below:
The model does not allow reads to be introduced, however, because this would imply refetching a value from memory, and in low-lock code memory could be changing.
BUT note under in the same page, under Technique 1: Avoiding Locks on Some Reads
In systems using the ECMA model, there is an additional subtlety. Even if only one memory location is fetched into a local variable and that local is used multiple times, each use might have a different value! This is because the ECMA model lets the compiler eliminate the local variable and re-fetch the location on each use. If updates are happening concurrently, each fetch could have a different value. This behavior can be suppressed with volatile declarations, but the issue is easy to miss.
If you are writing under Mono, you should be advised that at least until 2008 it was working on the ECMA memory model (or so they wrote in their mailing list)
It is legal optimization according to the memory model defined in the ECMA specification. If the _action were volatile, memory model would guarantee that the value is read only once and so this optimization could not happen.
However, I think that current Microsoft's CLR implementations do not optimize local variables away.
With C# 7, you should write the example as follows, and in fact the IDE will suggest it as a 'simplification' for you. The compiler will generate code that uses a temporary local to only read the location of _action
from main memory a single time (regardless of it being null or not), and this helps prevent the common race shown the OP's second example, i.e., where _action
is accessed twice, and can be set to null by another thread inbetween.
class Example
{
private Action _action;
private void InvokeAction()
{
this._action?.Invoke();
}
}
精彩评论