tags:

views:

108

answers:

1

As I understand it, the .NET memory model on a 32-bit machine guarantees 32-bit word writes and reads to be atomic operations but does not provide this guarantee on 64-bit words. I have written a quick tool to demonstrate this effect on a Windows XP 32-bit OS and am getting results consistent with that memory model description.

However, I have taken this same tool's executable and run it on a Windows 7 Enterprise 64-bit OS and am getting wildly different results. Both the machines are identical specs just with different OSes installed. I would have expected that the .NET memory model would guarantee writes and reads to BOTH 32-bit and 64-bit words to be atomic on a 64-bit OS. I find results completely contrary to BOTH assumptions. 32-bit reads and writes are not demonstrated to be atomic on this OS.

Can someone explain to me why this fails on a 64-bit OS?

Tool code:

using System;
using System.Threading;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            var th = new Thread(new ThreadStart(RunThread));
            var th2 = new Thread(new ThreadStart(RunThread));
            int lastRecordedInt = 0;
            long lastRecordedLong = 0L;
            th.Start();
            th2.Start();
            while (!done)
            {
                int newIntValue = intValue;
                long newLongValue = longValue;
                if (lastRecordedInt > newIntValue) Console.WriteLine("BING(int)! {0} > {1}, {2}", lastRecordedInt, newIntValue, (lastRecordedInt - newIntValue));
                if (lastRecordedLong > newLongValue) Console.WriteLine("BING(long)! {0} > {1}, {2}", lastRecordedLong, newLongValue, (lastRecordedLong - newLongValue));
                lastRecordedInt = newIntValue;
                lastRecordedLong = newLongValue;
            }
            th.Join();
            th2.Join();
            Console.WriteLine("{0} =? {2}, {1} =? {3}", intValue, longValue, Int32.MaxValue / 2, (long)Int32.MaxValue + (Int32.MaxValue / 2));
        }

        private static long longValue = Int32.MaxValue;
        private static int intValue;
        private static bool done = false;

        static void RunThread()
        {
            for (int i = 0; i < Int32.MaxValue / 4; ++i)
            {
                ++longValue;
                ++intValue;
            }
            done = true;
        }
    }
}

Results on Windows XP 32-bit:

Windows XP 32-bit
Intel Core2 Duo P8700 @ 2.53GHz
BING(long)! 2161093208 > 2161092246, 962
BING(long)! 2162448397 > 2161273312, 1175085
BING(long)! 2270110050 > 2270109040, 1010
BING(long)! 2270115061 > 2270110059, 5002
BING(long)! 2558052223 > 2557528157, 524066
BING(long)! 2571660540 > 2571659563, 977
BING(long)! 2646433569 > 2646432557, 1012
BING(long)! 2660841714 > 2660840732, 982
BING(long)! 2661795522 > 2660841715, 953807
BING(long)! 2712855281 > 2712854239, 1042
BING(long)! 2737627472 > 2735210929, 2416543
1025780885 =? 1073741823, 3168207035 =? 3221225470

Notice how BING(int) is never written and demonstrates that 32-bit reads/writes are atomic on this 32-bit OS.

Results on Windows 7 Enterprise 64-bit:

Windows 7 Enterprise 64-bit
Intel Core2 Duo P8700 @ 2.53GHz
BING(long)! 2208482159 > 2208121217, 360942
BING(int)! 280292777 > 279704627, 588150
BING(int)! 308158865 > 308131694, 27171
BING(long)! 2549116628 > 2548884894, 231734
BING(int)! 534815527 > 534708027, 107500
BING(int)! 545113548 > 544270063, 843485
BING(long)! 2710030799 > 2709941968, 88831
BING(int)! 668662394 > 667539649, 1122745
1006355562 =? 1073741823, 3154727581 =? 3221225470

Notice that BING(long) AND BING(int) are both displayed! Why are the 32-bit operations failing, let alone the 64-bit ones?

+2  A: 

In your thread callback you are doing much more than simply writing or reading:

++longValue;
++intValue;

Doing both reading and writing cannot be guaranteed to be atomic. Use Interlocked.Increment to ensure atomicity of this operation.

Darin Dimitrov
I do not want those lines to be explicitly atomic. I want the write operation to be guaranteed atomic by the memory model. That's what I'm trying to demonstrate. Obviously I should use Interlocked.Increment(ref intValue or longValue) but that would defeat the purpose of the demonstration.
James Dunne
The write operation is atomic, it's the read + write which isn't, as Darin said. So if you read the value 5 in thread one, increment it to 6, since it's not atomic thread 2 may have read value 5 in the meantime and already incremented it to 6 or more. So thread 1 is resetting the value back to a previous value, explaining the behavior you're seeing.
Julien Lebosquain
@Julien Yes, but the case being tested for in the main Console.WriteLine thread is that the last-recorded value should never be greater than the most recent value. Can you explain the large differences in values seen? That's the last value in the output lines. It's not off by one or two, it's on the order of 1,000 or 100,000, indicating some serious bit shearing going on.
James Dunne
Agreed. There's something *really* wrong with your 32-bit operating system. Or the machine has only one core.
Hans Passant
@Hans No, the 32-bit OS is behaving exactly as the memory model describes. It's the 64-bit case that is troubling me with its 32-bit read/write failures AND 64-bit read/write failures! Both machines tested with are of identical specs: Intel Core2 Duos, i.e. they both have two cores.
James Dunne
@James: you are really missing the point here. operator++() is *not* atomic. If it were then it would have been silly to implement Interlocked.Increment()
Hans Passant
@James: You are not necessarily seeing memory model issues here. You are more likely seeing a difference in thread scheduling in the 2 O/S's. As others have said -- the ++ operator is not atomic (see Julien's explanation in these comments). If you just change the timing of when any of the 3 threads you are using is preempted/awoken, you can account for the gaps -- you are not necessarily seeing bit shearing here. Just for kicks, I ran it on my quad-core machine (win7/64-bit), and I bet I got hundreds of BING!'s. (in both x86 and AnyCpu configurations)
JMarsch
@JMarsch: I do understand that ++ is not atomic. Your argument is basically that there are thread scheduling issues at hand that could account for the large variances. I can certainly see that as being the root cause. However, that does not explain why the `int` case on 32-bit does NOT fail and only the `long` case does. I will attempt a reengineering of my demonstration tool to record more data about thread scheduling and also to remove the (alleged) huge bottleneck of Console.WriteLine. Thanks for the response; that is helpful! :)
James Dunne