Thursday, November 12, 2009

Fixed used with anonymous delegates is tricky

I'm back (5 minutes later) to continue trying to understand a mystery with fixed. At work our problem didn't just involve fixed(), but also the usage of fixed pointers inside an anonymous delegate defined and called within the scope of the fixed block.

Andreas came up with a nice test that surprised us both when we saw the disassembly. Here is the code:


private delegate void Action();
private unsafe void FixedIsColdComfortToDelegates(int runNumber)
{
byte[] array = new byte[1];
fixed(byte *p = array)
{
Action action = () => { *p = 22; };
Thread.Sleep(10);
action();
}
if(22 != array[0])
{
Console.WriteLine("Experienced failure, run {0}!", runNumber);
}
}


The disassembly was interesting, basically the fixed() statement was ignored! The question is why. Ultimately I found out the bug:


You can cause a fixed statement to be ignored by the JIT compiler if you simply include an anonymous delegate in the fixed block which references the fixed pointer. There is no need to run the delegate. Any additional usages of the fixed pointer in the fixed block will not prevent the fixed block from being ignored.


Here is my test program:


using System;
using System.Threading;

namespace FixedTest
{
class FixedProblem
{
private unsafe void Changer(byte *p)
{
// Do some allocations
byte[] bigarray = new byte[1024*50];
bigarray[3] = 34;
GC.Collect();
*p = bigarray[3];
}

private unsafe void NormalFixedStatement(int runNumber)
{
byte[] array = new byte[100];
fixed(byte *p = array)
{
Changer(p + 1); // will change array[1] to 34
Thread.Sleep(10);
*p = 10;
}

if(array[1] != 34)
{
Console.WriteLine("Experienced failure, run {0}", runNumber);
}
}

private unsafe void NullifiedFixedStatement(int runNumber)
{
byte[] array = new byte[100];
fixed (byte* p = array)
{
Action action = () => { *p = 10; }; // This destroys the fixed statement
Changer(p + 1); // will change array[1] to 34
Thread.Sleep(10);
*p = 10; // but p isn't fixed!
}

if (array[1] != 34)
{
Console.WriteLine("Experienced failure, run {0}", runNumber);
}
}

static void Main(string[] args)
{
FixedProblem p = new FixedProblem();

int count = 500;

Console.WriteLine("First run a function with a fixed statement and no anonymous delegate that uses the fixed pointer");
for (int i = 0; i < count; i++)
{
p.NormalFixedStatement(i);
}

Console.WriteLine("Now run a function with a fixed statement and an anonymous delegate that uses the fixed pointer");
for (int i = 0; i < count; i++)
{
p.NullifiedFixedStatement(i);
}
}
}
}


Here is the output:

C:\FixedTest\bin\Release>fixedtest.exe
First run a function with a fixed statement and no anonymous delegate
that uses the fixed pointer.


Now run a function with a fixed statement and an anonymous delegate that
uses the fixed pointer.

Experienced failure, run 4
Experienced failure, run 8
Experienced failure, run 12
Experienced failure, run 16
Experienced failure, run 20
...


I wonder if the CLR team knows about this bug?

Update: My friend Josef points out that if you introduce a temporary variable and pass that in to the anonymous delegate then fixed protection is restored. Something like:


...
byte *pTemp = p;
Action action = () => { *pTemp = 10; }
...

0 Comments:

Post a Comment

<< Home