5

Why does the getter Val happen to simulate volatility for the field val?

I'm assuming that leveraging a method call is not a reliable way to keep the variable volatile.

(To try it out, build for release and execute directly without the debugger.)

class Program
{
    private int val = 0;
    public int Val { get { return val; } }

    public static void Main()
    {
        var example = new Program();

        Task.Run(() => example.val++);

        while (example.val == 0) ; // Hangs if val is not volatile
        while (example.Val == 0) ; // Never seems to hang
    }
}
jnm2
  • 7,960
  • 5
  • 61
  • 99
  • 1
    It doesn't "simulate volatility". It "happens to work" :-) – Cameron Jul 15 '14 at 14:55
  • Simulate: [reproduce features of, fake, mimic](https://www.bing.com/search?q=define+simulate). – jnm2 Jul 15 '14 at 14:56
  • Fair enough (though volatile only has to do with memory ordering, not the propagation of the value itself from one thread to another). Anyway, it's a good question -- the value of val should propagate to the main thread eventually, but that doesn't seem to be happening. – Cameron Jul 15 '14 at 14:58
  • Ah... so my test is testing the compiler instead of the threading issues. I need a new kind of test. – jnm2 Jul 15 '14 at 15:09
  • Check out related http://stackoverflow.com/questions/17907885/c-sharp-compiler-optimization-and-volatile-keyword?rq=1 ... *I think* at some point I've seen article about change that disabled inlining/optimization in this particular case in one of the versions (could be dreaming too :) ). Try 32/64, different version of .Net and make sure to compile release to see if this code hangs. (Note: I suggest such code should be used strictly for entertainment purposes, in real code use code that shows correctness by proper locking/`Interlocked` calls). – Alexei Levenkov Jul 15 '14 at 15:11
  • 1
    It is an x86 jitter optimizer implementation detail for this code, x86 doesn't have a lot of registers. You can easily get it to hang on the property getter as well by running this code in 64-bit mode. Showing again that unsynchronized access to shared variables is evil. – Hans Passant Jul 15 '14 at 15:21

1 Answers1

5

Alright, it turns out that the jitter is allowed to assume that all non-volatile variables are only accessed by one thread (much like in the C++11 memory model where concurrent accesses to non-std::atomic<> variables invoke undefined behaviour). In this case, the jitter is optimizing that first loop into loop: test eax, eax; je loop (it hoists the variable access into a register that is never updated), so obviously it never terminates.

The second loop generates assembly that reads the value relative to the object's pointer, so eventually it sees the new value (though possibly out of order with respect to other writes on the other thread, again because the variable is not volatile). This is coincidental due to the assembly that happened to be generated.

x86 assembly generated for first (infinite) loop:

003B23BA  test        eax,eax  
003B23BC  je          003B23BA  

x86 assembly for second (finite) loop:

002F2607  cmp         dword ptr [eax+4],0  
002F260B  je          002F2607

Since the jitter is allowed to assume non-volatile variables are never touched by other threads, you can only rely on volatile to work as expected (even if it appears to in a given circumstance, like this one, because future optimisations (or different CPU architectures, etc.) will probably break your code in difficult-to-debug ways).

Cameron
  • 96,106
  • 25
  • 196
  • 225
  • 1
    I don't think it is interesting part of the question - clearly first one can be optimized... The question seem to be why property is not optimized and if it is reliable replacement for `volatile`. – Alexei Levenkov Jul 15 '14 at 15:13
  • Does the jitter know that `Task.Run` is working async? I don't think so! – Felix K. Jul 15 '14 at 15:15
  • @Alexei: The property getter method is inlined, but the variable is re-read every time through the loop instead of being put once into a register. – Cameron Jul 15 '14 at 15:17
  • @Felix: Right, it doesn't. I'm not sure I follow? – Cameron Jul 15 '14 at 15:19
  • Because initialy you said he creates a `while (true)` out of the first loop. But how could he know that `Task.Run(() => example.val++);` won't change the value of val before he enters the while. – Felix K. Jul 15 '14 at 15:21
  • @Felix: The jitter doesn't know that `Task.Run` won't change the value -- it reads it into `eax` after the `Run` method returns, but before it enters the loop. Since it assumes the variable is only modified on that thread, and it knows the variable is not modified during the loop, this is a perfectly correct optimization. – Cameron Jul 15 '14 at 15:27
  • Yes i know, but its not a `while (true)` that was my point. Everything else you said makes totaly sence. – Felix K. Jul 15 '14 at 15:43
  • @FelixK. the compiler isn't smart enough to understand that the Task.Run affects the loop. The first sentence of this answer: *the jitter is allowed to assume that all non-volatile variables are only accessed by one thread.* If there was only one thread, then the compiler would be correct to substitute `0` for `example.val`. `while (0 == 0)`, `while (true)`. – jnm2 Jul 15 '14 at 15:49
  • The way this reads right now you seem to be claiming that an optimizing compiler for c++ cannot assume that only a single thread will change a variable.. that doesn't make much sense.The compiler has even more options in c++ than c#since data races in c++ are undefined behavior! – Voo Jul 15 '14 at 21:44
  • @Voo: Hah, good catch. I was thinking of `std::atomic<>` variables, which have well-defined semantics, much like `volatile` in C# -- but of course, that doesn't apply to *all* variables. I'll edit my answer, thanks. – Cameron Jul 16 '14 at 02:11