C# 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 特有的东西。
- 使用日期时间或小数类型时会出现问题。显然,它适用于 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 */
}
}
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(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);
}
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 */
}
}
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(1)
查看代码的这一部分:
在第一行之后,堆栈顶部将包含两个对象引用。然后,您有条件分支,删除其中一个引用。下一行取消对十进制值的引用。因此,在标记标签的地方,堆栈顶部要么是对象引用(如果采用了分支),要么是十进制值(如果没有采用)。这些堆栈状态不兼容。
编辑
正如您在评论中指出的那样,如果堆栈状态顶部有小数或顶部有对象引用,则下面的 IL 代码将起作用,因为它只是将值弹出无论哪种方式堆栈。但是,您尝试做的事情仍然行不通(根据设计):每条指令都需要有一个堆栈状态。请参阅 ECMA 的第 1.8.1.3 节(合并堆栈状态) CLI 规范 了解更多详细信息。
Look at this part of your code:
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.