Возможная ошибка в оптимизаторе C # JIT? - PullRequest
23 голосов
/ 11 мая 2011

Работая над классом SQLHelper для автоматизации вызовов хранимых процедур аналогично тому, как это делается в библиотеке XmlRpc.Net , я столкнулся с очень странной проблемой при запуске метода, сгенерированного вручную из кода IL ,

Я сузил его до простого сгенерированного метода (возможно, он мог бы быть упрощен еще больше). Я создаю новую сборку и тип, содержащий два метода для соответствия

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

Методы тестирования просто загружают десятичный аргумент в стек, помещают его в коробку, проверяют, имеет ли он значение NULL, а если нет, распаковывают его.

Генерация метода TestOk () выглядит следующим образом:

static void BuildMethodOk(TypeBuilder tb)
{
    /* Create a method builder */
    MethodBuilder mthdBldr = tb.DefineMethod( "TestOk", 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 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);
}

Здание для TestWrong () почти идентично:

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);
}

Единственная разница в том, что я использую BrFalse вместо BrTrue , чтобы проверить, является ли значение в стеке нулевым.

Теперь запустим следующий код:

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

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

SimpleCodeGen.Create () создает новую сборку и тип и вызывает вышеупомянутый BuildMethodXX для генерации кода для TestOk и TestWrong. Это работает должным образом: ничего не делает, значение dectest не изменяется. Тем не менее, работает:

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

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

значение dectest повреждено (иногда оно получает большое значение, иногда оно говорит "неверное десятичное значение", ...), и программа вылетает.

Может ли это быть ошибкой в ​​JIT или я что-то не так делаю?

Некоторые подсказки:

  • В отладчике это происходит только тогда, когда «Подавить оптимизацию JIT» отключено. Если «Подавить оптимизацию JIT» включен, он работает. Это заставляет меня думать, что проблема должна быть в оптимизированном коде JIT.
  • Запуск такого же теста на Mono 2.4.6, он работает как положено, так что это что-то особенное для Microsoft .NET.
  • Проблема появляется при использовании даты и времени или десятичных типов. По-видимому, это работает для int или для ссылочных типов (для ссылочных типов сгенерированный код не идентичен, но я опускаю этот случай, когда он работает).
  • Я думаю эта ссылка , о которой сообщалось давно, может быть связана.
  • Я пробовал .NET Framework v2.0, v3.0, v3.5 и v4, и поведение точно такое же.

Я опускаю остальную часть кода, создавая сборку и тип. Если вам нужен полный код, просто спросите меня.

Большое спасибо!

Редактировать: я включаю остальную часть сборки и код создания типа, для завершения:

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 */           
    }
}

1 Ответ

7 голосов
/ 11 мая 2011

Посмотрите на эту часть вашего кода:

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

После первой строки вершина стека будет содержать две ссылки на объекты.Вы тогда условно переходите, удаляя одну из ссылок.Следующая строка распаковывает ссылку на значение decimal.Поэтому, где вы помечаете свою метку, вершина стека является либо ссылкой на объект (если ветвь была взята), либо десятичным значением (если это не так).Эти состояния стека несовместимы.

РЕДАКТИРОВАТЬ

Как вы указали в своем комментарии, ваш код IL, следующий за этим, будет работать, если состояние стека будет иметь десятичную дробь сверхуили если он имеет ссылку на объект сверху, так как он просто выталкивает значение из стека в любом случае.Однако то, что вы пытаетесь сделать, все равно не сработает (по замыслу): в каждой инструкции должно быть одно состояние стека.См. Раздел 1.8.1.3 (Слияние состояний стека) спецификации ECMA CLI для получения более подробной информации.

...