views:

670

answers:

5

I came across a bug in code that is only reproduced when the code is built with optimizations enabled. I've made a console app that replicates the logic for testing (code below). You'll see that when optimization is enabled 'value' becomes null after execution of this invalid logic:

if ((value == null || value == new string[0]) == false)

The fix is straight forward and is commented out below the offending code. But... I'm more concerned that I may have come across a bug in the assembler or perhaps someone else has an explanation of why value gets set to null.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace memory_testing
{
    class Program
    {
        sta tic void Main(string[] args)
        {
            while(true)
            {
                Console.Write("Press any key to start...");
                Console.ReadKey();
                Console.WriteLine();
                PrintManagerUser c = new PrintManagerUser();
                c.MyProperty = new string[1];
            }
        }
    }

    public class PrintManager
    {
        public void Print(string key, object value)
        {
            Console.WriteLine("Key is: " + key);
            Console.WriteLine("Value is: " + value);
        }
    }

    public class PrintManagerUser
    {
        public string[] MyProperty
        {
            get { return new string[100]; }
            set
            {
                Console.WriteLine("Pre-check Value is: " + value);
                if ((value == null || value == new string[0]) == false)
                {
                    Console.WriteLine("Post-check Value is: " + value);
                    new PrintManager().Print("blah", value);
                }
                //if (value != null && value.Length > 0)
                //{
                //    new PrintManager().Print("blah", value);
                //}
            }
        }
    }
}

The normal output should be:

Pre-check Value is: System.String[]
Post-check Value is: System.String[]
Key is: blah
Value is: System.String[]

The buggy output is:

Pre-check Value is: System.String[]
Post-check Value is:
Key is: blah
Value is:   

My Env is a VM running Windows Server 2003 R2 with .NET 3.5 SP1. Using VS2008 Team System.

Thanks,

Brian

+1  A: 

I'm on x64 and couldn't reproduce the problem at first. Then I specified the target as x86, and it happened to me. Back to x64, and it went away. Not sure what this means, exactly, but I've gone back and forth a few times now.

Tim Coker
I'm on x64 as well and can't reproduce this with optimization =true and target=x86.
nos
+2  A: 
value == new string[0]

The above looks like a weird statement to me. You are comparing two string arrays with an equals statement. That will only result in true if they both point to the same array, which is quite unlikely. This doesn't explain yet why this code behaves differently in an optimized version.

Maurits Rijk
Cant think of any way to inject an operator overload for that, but is that definitely 100% true me wonders? In the meantime, that what I thought, so +1
Ruben Bartelink
+31  A: 

Yes, your expression fatally confuses the JIT optimizer. The generated machine code looks like this:

                if ((value == null || value == new string[0]) == false)
00000027  test        esi,esi               ; value == null?
00000029  je          00000075 
0000002b  xor         edx,edx               ; new string[0]
0000002d  mov         ecx,6D913BD2h 
00000032  call        FFD20BC8 
00000037  cmp         eax,esi               ; (value == new string[0]) == false?
00000039  je          00000075 
                {
                    Console.WriteLine("Post-check Value is: " + value);
0000003b  mov         ecx,dword ptr ds:[03532090h]  ; "Post-check value is: "
00000041  xor         edx,edx               ; BUGBUG not null!
00000043  call        6D70B7E8              ; String.Concat()
00000048  mov         esi,eax               ; 
0000004a  call        6D72BE08              ; get Console.Out
0000004f  mov         ecx,eax 
00000051  mov         edx,esi 
00000053  mov         eax,dword ptr [ecx] 
00000055  call        dword ptr [eax+000000D8h]     ; Console.WriteLine()

The bug occurs at address 41, the optimizer has concluded that value will always be null so it directly passes a null to String.Concat().

For comparison, this is the code that's generated when JIT optimization is turned off:

                    Console.WriteLine("Post-check Value is: " + value);
00000056  mov         ecx,dword ptr ds:[03342090h] 
0000005c  mov         edx,dword ptr [ebp-8] 
0000005f  call        6D77B790 

The code got moved, but do note that at address 5c it now uses the local variable (value) instead of null.

You can report this bug at connect.microsoft.com. The workaround is simple:

  if (value != null)
  {
    Console.WriteLine("Post-check Value is: " + value);
    new PrintManager().Print("blah", value);
  }
Hans Passant
Thanks for the great explanation of what's going on.
bmancini
this is easily the most impressive answer I've ever seen on SO... Or maybe I'm just easily impressed...
Ben
A: 

Certainly looks like a bug, does it reproduce when you swap the operator operands like this?

if (false == (null == value || new string[0] == value))
rsp
+3  A: 

This bug seems to have been fixed in .NET 4 (beta 2). Here's the optimized x86 disassembly for the bit nobugz highlighted, above:

                    Console.WriteLine("Post-check Value is: " + value);
00000056  mov         ecx,dword ptr ds:[033C2090h] 
0000005c  mov         edx,dword ptr [ebp-8] 
0000005f  call        65D8FE10

The program also displays the expected output in both optimized and unoptimized modes.

ladenedge