Reputation: 13396
EDIT: I've finally written a complete article about the issue: Synchronization, memory visibility and leaky abstractions
I'm demonstrating the importance of volatile read with this code:
bool ok = false;
void F()
{
int n = 0;
while (!ok) ++n;
}
public void Run()
{
Thread thread = new Thread(F);
thread.Start();
Console.Write("Press enter to notify thread...");
Console.ReadLine();
ok = true;
Console.WriteLine("Thread notified.");
}
As expected the thread is not aware of the new ok
value and the program hangs.
But to obtain this behavior I have to do something in the while
loop, e.g. incrementing an integer.
If I remove the ++n
statement, the thread reads the new value and exits.
I guess it has something to do with the JITter optimizations because as far as CIL is concerned there is nothing (at least for a layman like me):
.method private hidebysig instance void F() cil managed
{
.maxstack 2
.locals init ([0] int32 n)
IL_0000: ldc.i4.0
IL_0001: stloc.0
IL_0002: br.s IL_0008
IL_0004: ldloc.0
IL_0005: ldc.i4.1
IL_0006: add
IL_0007: stloc.0
IL_0008: ldarg.0
IL_0009: ldfld bool ThreadingSamples.MemoryVisibilitySample::ok
IL_000e: brfalse.s IL_0004
IL_0010: ret
}
.method private hidebysig instance void F() cil managed
{
.maxstack 8
IL_0000: ldarg.0
IL_0001: ldfld bool ThreadingSamples.MemoryVisibilitySample::ok
IL_0006: brfalse.s IL_0000
IL_0008: ret
}
And, on the contrary, I would naively expect that doing something in the loop would increase the odds for the thread to trigger a cache refresh.
What am I missing again?
FINAL EDIT: this is again some JITter black-magic.
Kudos to Hans for confirming this is a "well-known" JITter "issue" and for pointing out that in x64 we get the "expected" behavior.
Kudos to MagnatLU for providing the resulting assembly code and for sharing some debugging wisdom.
Upvotes: 1
Views: 204
Reputation:
As you wrote, it's all in the JITter. In Release build and without debugger attached, with ++n
you get:
int n = 0;
00000000 push ebp
00000001 mov ebp,esp
while (!ok) ++n;
00000003 movzx eax,byte ptr [ecx+4]
00000007 test eax,eax
00000009 jne 0000000F
0000000b test eax,eax ; <---
0000000d je 0000000B ; <---
0000000f pop ebp
}
00000010 ret
And without ++n
:
while (!ok) ;
00000000 push ebp
00000001 mov ebp,esp
00000003 cmp byte ptr [ecx+4],0
00000007 je 00000003
00000009 pop ebp
}
0000000a ret
The real question should be why there is no code for ++n
emitted at all.
Edit: on x64 Release build results are similar:
Debugger.Break();
00000000 push rbx
00000001 sub rsp,20h
00000005 mov rbx,rcx
00000008 call FFFFFFFFED0EE4D0
0000000d mov ecx,2710h
00000012 call FFFFFFFFEDCFE460
while (!ok) ++n;
00000017 mov al,byte ptr [rbx+8]
0000001a movzx ecx,al
0000001d test ecx,ecx
0000001f jne 0000000000000025
00000021 test ecx,ecx
00000023 je 0000000000000021
00000025 add rsp,20h
00000029 pop rbx
0000002a rep ret
Upvotes: 3