Подтвердить что ты не робот

Возможная ошибка в оптимизаторе CIT JIT?

Работа над классом 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.
  • Проблема возникает при использовании типов datetime или decimal. По-видимому, он работает для 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 */           
    }
}
4b9b3361

Ответ 1

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

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 (Слияние состояний стека)