为什么JIT编译器不会删除未使用的变量?

时间:2016-10-26 14:11:22

标签: c# optimization

最近对我的回答的评论表明,变量创建了两次。

起初,我开始撰写以下评论:

  

我很确定.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没有通过删除未使用的变量及其分配来优化原始代码的原因是什么?

3 个答案:

答案 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 。为了做到这一点,它必须能够解决暂停问题,这一般是不可判定的。