开发者

Possible bug in C# JIT optimizer?

开发者 https://www.devze.com 2023-03-05 05:35 出处:网络
Working on a SQLHelper class to automate stored procedures calls in a similar way to what is done in the XmlRpc.Net library, I have hit a very strange problem when running a method generated manually

Working on a SQLHelper class to automate stored procedures calls in a similar way to what is done in the XmlRpc.Net library, I have hit a very strange problem when running a method generated manually from IL code.

I've narrowed it down to a simple generated method (probably it could be simplified even more). I create a new assembly and type, containing two methods to comply with

public interface iTestDecimal
{
    void TestOk(ref decimal value);
    void TestWrong(ref decimal value);
}

The test methods are just loading the decimal argument into the stack, boxing it, checking if it's NULL, and if it is not, unboxing it.

The generation of TestOk() method is as follows:

static void BuildMethodOk(TypeBuilder tb)
{
    /* Create a method builder */
    MethodBuilder mthdBldr = tb.DefineMethod( "TestOk", MethodAttributes.Public | MethodAttributes.Virtual,
      typeof(v开发者_Go百科oid), new Type[] {typeof(decimal).MakeByRefType() });

    ParameterBuilder paramBldr = mthdBldr.DefineParameter(1,  ParameterAttributes.In | ParameterAttributes.Out, "value");
    // generate IL
    ILGenerator ilgen = mthdBldr.GetILGenerator();

    /* Load argument to stack, and box the decimal value */
    ilgen.Emit(OpCodes.Ldarg, 1);

    ilgen.Emit(OpCodes.Dup);
    ilgen.Emit(OpCodes.Ldobj, typeof(decimal));
    ilgen.Emit(OpCodes.Box, typeof(decimal));

    /* Some things were done in here, invoking other method, etc */
    /* At the top of the stack we should have a boxed T or null */

    /* Copy reference values out */

    /* Skip unboxing if value in the stack is null */
    Label valIsNotNull = ilgen.DefineLabel();
    ilgen.Emit(OpCodes.Dup);

    /* This block works */
    ilgen.Emit(OpCodes.Brtrue, valIsNotNull);
    ilgen.Emit(OpCodes.Pop);
    ilgen.Emit(OpCodes.Pop);
    ilgen.Emit(OpCodes.Ret);
    /* End block */

    ilgen.MarkLabel(valIsNotNull);
    ilgen.Emit(OpCodes.Unbox_Any, typeof(decimal));

    /* Just clean the stack */
    ilgen.Emit(OpCodes.Pop);
    ilgen.Emit(OpCodes.Pop);
    ilgen.Emit(OpCodes.Ret);
}

The building for TestWrong() is nearly identical:

static void BuildMethodWrong(TypeBuilder tb)
{
    /* Create a method builder */
    MethodBuilder mthdBldr = tb.DefineMethod("TestWrong", MethodAttributes.Public | MethodAttributes.Virtual,
    typeof(void), new Type[] { typeof(decimal).MakeByRefType() });

    ParameterBuilder paramBldr = mthdBldr.DefineParameter(1,  ParameterAttributes.In | ParameterAttributes.Out, "value");

    // generate IL
    ILGenerator ilgen = mthdBldr.GetILGenerator();

    /* Load argument to stack, and box the decimal value */
    ilgen.Emit(OpCodes.Ldarg, 1);
    ilgen.Emit(OpCodes.Dup);
    ilgen.Emit(OpCodes.Ldobj, typeof(decimal));
    ilgen.Emit(OpCodes.Box, typeof(decimal));

    /* Some things were done in here, invoking other method, etc */
    /* At the top of the stack we should have a boxed decimal or null */

    /* Copy reference values out */

    /* Skip unboxing if value in the stack is null */
    Label valIsNull = ilgen.DefineLabel();
    ilgen.Emit(OpCodes.Dup);

    /* This block fails */
    ilgen.Emit(OpCodes.Brfalse, valIsNull);
    /* End block */

    ilgen.Emit(OpCodes.Unbox_Any, typeof(decimal));
    ilgen.MarkLabel(valIsNull);

    /* Just clean the stack */
    ilgen.Emit(OpCodes.Pop);
    ilgen.Emit(OpCodes.Pop);
    ilgen.Emit(OpCodes.Ret);
}

The only difference is I'm using BrFalse instead of BrTrue to check if the value in the stack is null.

Now, running the following code:

iTestDecimal testiface = (iTestDecimal)SimpleCodeGen.Create();

decimal dectest = 1;
testiface.TestOk(ref dectest);
Console.WriteLine(" Dectest: " + dectest.ToString());

The SimpleCodeGen.Create() is creating a new assembly and type, and calling the BuildMethodXX above to generate the code for TestOk and TestWrong. This works as expected: does nothing, value of dectest is not changed. However, running:

iTestDecimal testiface = (iTestDecimal)SimpleCodeGen.Create();

decimal dectest = 1;
testiface.TestWrong(ref dectest);
Console.WriteLine(" Dectest: " + dectest.ToString());

the value of dectest is corrupted (sometimes it gets a big value, sometimes it says "invalid decimal value", ...) , and the program crashes.

May this be a bug in the JIT, or am I doing something wrong?

Some hints:

  • In debugger, it happens only when "Suppress JIT optimizations" is disabled. If "Suppress JIT optimizations" is enabled, it works. This makes me think the problem must be in the JIT optimized code.
  • Running the same test on Mono 2.4.6 it works as expected, so this is something specific for Microsoft .NET.
  • Problem appears when using datetime or decimal types. Apparently, it works for int, or for reference types (for reference types, the generated code is not identical, but I'm omiting that case as it works).
  • I think this link, reported long time ago, might be related.
  • I've tried .NET framework v2.0, v3.0, v3.5 and v4, and behavior is exactly the same.

I'm omitting the rest of the code, creating the assembly and type. If you want the full code, just ask me.

Thanks very much!

Edit: I'm including the rest of the assembly and type creation code, for completion:

class SimpleCodeGen
{
    public static object Create()
    {
        Type proxyType;

        Guid guid = Guid.NewGuid();
        string assemblyName = "TestType" + guid.ToString();
        string moduleName = "TestType" + guid.ToString() + ".dll";
        string typeName = "TestType" + guid.ToString();

        /* Build the new type */
        AssemblyBuilder assBldr = BuildAssembly(typeof(iTestDecimal), assemblyName, moduleName, typeName);
        proxyType = assBldr.GetType(typeName);
        /* Create an instance */
        return Activator.CreateInstance(proxyType);
    }

    static AssemblyBuilder BuildAssembly(Type itf, string assemblyName, string moduleName, string typeName)
    {
        /* Create a new type */
        AssemblyName assName = new AssemblyName();
        assName.Name = assemblyName;
        assName.Version = itf.Assembly.GetName().Version;
        AssemblyBuilder assBldr = AppDomain.CurrentDomain.DefineDynamicAssembly(assName, AssemblyBuilderAccess.RunAndSave);
        ModuleBuilder modBldr = assBldr.DefineDynamicModule(assName.Name, moduleName);
        TypeBuilder typeBldr = modBldr.DefineType(typeName,
          TypeAttributes.Class | TypeAttributes.Sealed | TypeAttributes.Public, 
          typeof(object), new Type[] { itf });

        BuildConstructor(typeBldr, typeof(object));
        BuildMethodOk(typeBldr);
        BuildMethodWrong(typeBldr);
        typeBldr.CreateType();
        return assBldr;
    }

    private static void BuildConstructor(TypeBuilder typeBldr, Type baseType)
    {
        ConstructorBuilder ctorBldr = typeBldr.DefineConstructor(
          MethodAttributes.Public | MethodAttributes.SpecialName |
          MethodAttributes.RTSpecialName | MethodAttributes.HideBySig,
          CallingConventions.Standard,
          Type.EmptyTypes);

        ILGenerator ilgen = ctorBldr.GetILGenerator();
        //  Call the base constructor.
        ilgen.Emit(OpCodes.Ldarg_0);
        ConstructorInfo ctorInfo = baseType.GetConstructor(System.Type.EmptyTypes);
        ilgen.Emit(OpCodes.Call, ctorInfo);
        ilgen.Emit(OpCodes.Ret);
    }

    static void BuildMethodOk(TypeBuilder tb)
    {
        /* Code included in examples above */
    }

    static void BuildMethodWrong(TypeBuilder tb)
    {
        /* Code included in examples above */           
    }
}


Look at this part of your code:

ilgen.Emit(OpCodes.Dup);
ilgen.Emit(OpCodes.Brfalse, valIsNull);
ilgen.Emit(OpCodes.Unbox_Any, typeof(decimal));
ilgen.MarkLabel(valIsNull);

After the first line, the top of the stack will contain two object references. You then conditionally branch, removing one of the references. The next line unboxes the reference to a decimal value. So where you mark your label, the top of the stack is either an object reference (if the branch was taken) or a decimal value (if it wasn't). These stack states are not compatible.

EDIT

As you point out in your comment, your IL code following this would work if the stack state has a decimal on top or if it has an object reference on top, since it just pops the value off of the stack either way. However, what you're trying to do still won't work (by design): there needs to be a single stack state at each instruction. See section 1.8.1.3 (Merging stack states) of the ECMA CLI spec for more details.

0

精彩评论

暂无评论...
验证码 换一张
取 消