最近对我的回答的评论表明,变量创建了两次。
起初,我开始撰写以下评论:
我很确定.NET的JIT编译器会通过将两个变量的声明移动到它们实际使用的位置来重写代码。 [...]
但后来我决定检查我的说法。令我惊讶的是,看起来我显然是错的。
让我们从以下代码开始:
class Something
{
public string text;
public int number;
public Something(string text, int number)
{
Console.WriteLine("Initialized {0}.", number);
this.text = text;
this.number = number;
}
}
static void Display(Something something)
{
Console.WriteLine(something.text, something.number);
}
static int x = 0;
public static void Main()
{
var first = new Something("Hello, {0}!", 123);
var second = new Something("World, {0}!", 456);
Display(x > 0 ? first : second);
}
警告:代码是POC,并且存在严重的样式问题,例如公共字段;不要在原型之外编写这样的代码。
输出如下:
Initialized 123.
Initialized 456.
World, 456!
让我们稍微改变Main()
方法:
void Main()
{
Display(
x > 0 ?
new Something("Hello, {0}!", 123) :
new Something("World, {0}!", 456));
}
现在输出变为:
Initialized 456.
World, 456!
顺便说一句,如果我查看修改后的版本的IL,那两个newobj
指令仍然存在:
IL_0000: ldarg.0 IL_0001: ldarg.0 IL_0002: ldfld UserQuery.x IL_0007: ldc.i4.0 IL_0008: bgt.s IL_001B IL_000A: ldstr "World, {0}!" IL_000F: ldc.i4 C8 01 00 00 IL_0014: newobj UserQuery+Something..ctor IL_0019: br.s IL_0027 IL_001B: ldstr "Hello, {0}!" IL_0020: ldc.i4.s 7B IL_0022: newobj UserQuery+Something..ctor IL_0027: call UserQuery.Display IL_002C: ret
这意味着编译器保持两个初始化指令不变,但JIT通过只保留一个来优化它们。
JIT没有通过删除未使用的变量及其分配来优化原始代码的原因是什么?
答案 0 :(得分:4)
在写这个问题时,我觉得答案非常简单。 JIT optimizations仅限于被认为是安全的,随机删除对构造函数的调用几乎是安全的,因为构造函数可能有副作用,实际上有> em>示例代码中的副作用,因为它向控制台显示一条消息。
优化(和缺乏)可以更容易说明:
static string Create(string name)
{
Console.WriteLine(name);
return name;
}
public static void Main()
{
var first = Create("Jeff");
var second = Create("Alice");
Console.WriteLine("Hello, {0}!", second);
}
此代码将输出:
Created Jeff.
Created Alice.
Hello, Alice!
JIT编译器成功地理解该方法具有副作用 - 输出到控制台 - 并且不会删除第一个调用,即使从未使用过first
。这是相应的汇编代码:
006B2DA8 push ebp
006B2DA9 mov ebp,esp
006B2DAB mov ecx,dword ptr ds:[335230Ch]
006B2DB1 call dword ptr ds:[4770860h] // Displays "Jeff" to console.
006B2DB7 mov ecx,dword ptr ds:[3352310h]
006B2DBD call dword ptr ds:[4770860h] // Displays "Alice" to console.
006B2DC3 mov ecx,dword ptr ds:[3352314h]
006B2DC9 mov edx,eax
006B2DCB call 702AE044 // Displays "Hello, Alice!" to console.
006B2DD0 pop ebp
006B2DD1 ret
对这段代码稍作修改会产生截然不同的结果。通过删除Console.WriteLine()
中的Create()
语句,JIT现在假定Create()
几乎不返回参数的值。虽然IL代码仍然包含对Create()
方法的两次调用:
IL_0000: ldc.i4.s 123 IL_0002: call int32 ConsoleApplication4.Program::Create(int32) IL_0007: pop IL_0008: ldc.i4 456 IL_000d: call int32 ConsoleApplication4.Program::Create(int32) IL_0012: stloc.0 IL_0013: ldstr "Hello, {0}!" IL_0018: ldloc.0 IL_0019: box [mscorlib]System.Int32 IL_001e: call void [mscorlib]System.Console::WriteLine(string, object) IL_0023: ret
JIT编译器摆脱了第一次调用,生成的汇编代码现在要短得多:
00F12DA8 mov edx,dword ptr ds:[3AA2310h]
00F12DAE mov ecx,dword ptr ds:[3AA2314h]
00F12DB4 call 702AE044
00F12DB9 ret
答案 1 :(得分:0)
首先,我想将此作为一个答案,因为它太复杂而不能作为评论imo。
如果您指定x
为静态,则JIT会生成所述结果,但如果您将x
指定为const
,该怎么办?你观察到相同的结果吗?
如果您明确键入0 > 0 ? :
与你的回答相关我觉得插入JIT没有删除它也是因为如果你插入一个断点并手动将x改为1,那么程序会以一种惊人的方式崩溃。 / p>
答案 2 :(得分:0)
我不确定JIT优化与此处的任何内容有什么关系。您提供的两个示例表现出不同的行为,因为它们不同,不是因为发生了某种JIT优化。
当你写:
var first = new Something("Hello, {0}!", 123);
var second = new Something("World, {0}!", 456);
Display(x > 0 ? first : second);
这会创建两个执行每个构造函数的Something
个对象,并在每个变量中存储对每个构造的引用。然后使用三元组选择要传递给Display
方法的那些。编写代码的方式,必须执行两个构造函数,以后只能使用其中一个实例并不重要。
Display(
x > 0 ?
new Something("Hello, {0}!", 123) :
new Something("World, {0}!", 456));
这完全不同。现在你有一个三元组,它只会在条件之后评估它的一个操作数。
顺便说一句,如果我查看修改后的版本的IL,那两个
newobj
指令仍然存在:
他们当然是,他们为什么不呢? IL加载静态x
字段并将其与0进行比较。如果x > 0
则代码执行跳过 over 第一个newobj
操作码,因此它永远不会被执行。而是执行第二个。如果是x <= 0
,它会执行第一个newobj
,然后跳过过第二个。
两个操作码都必须存在,因为有两件事可能发生。仅仅因为它们出现在IL并不意味着它们都被执行了。
编译器无法摆脱它,因为当它编译Main
方法时,在运行时调用方法时,它无法知道静态字段x
将为0 。为了做到这一点,它必须能够解决暂停问题,这一般是不可判定的。