C#中循环中的无用变量如何与捕获委托一起反汇编?

5

我试图查看这个旧问题中发布的代码的反汇编情况,并发现了一些奇怪的东西。

为了清晰起见,这里是源代码:

class ThreadTest
{
    static void Main(string[] args)
    {
        for (int i = 0; i < 10; i++)
            new Thread(() => Console.WriteLine(i)).Start();
    }
}

当然,这个程序的行为是出乎意料的,但这不是问题所在。
以下是我查看反汇编代码时看到的内容:
internal class ThreadTest
{
    private static void Main(string[] args)
    {
        int i;
        int j;
        for (i = 0; i < 10; i = j + 1)
        {
            new Thread(delegate
            {
                Console.WriteLine(i);
            }).Start();
            j = i;
        }
    }
}

这里“j”是做什么用的?以下是字节码:

.method private hidebysig static 
    void Main (
        string[] args
    ) cil managed 
{
    // Method begins at RVA 0x2050
    // Code size 64 (0x40)
    .maxstack 2
    .entrypoint
    .locals init (
        [0] class ConsoleApplication2.ThreadTest/'<>c__DisplayClass0_0' 'CS$<>8__locals0',
        [1] int32
    )

    IL_0000: newobj instance void ConsoleApplication2.ThreadTest/'<>c__DisplayClass0_0'::.ctor()
    IL_0005: stloc.0
    IL_0006: ldloc.0
    IL_0007: ldc.i4.0
    IL_0008: stfld int32 ConsoleApplication2.ThreadTest/'<>c__DisplayClass0_0'::i
    IL_000d: br.s IL_0035
    // loop start (head: IL_0035)
        IL_000f: ldloc.0
        IL_0010: ldftn instance void ConsoleApplication2.ThreadTest/'<>c__DisplayClass0_0'::'<Main>b__0'()
        IL_0016: newobj instance void [mscorlib]System.Threading.ThreadStart::.ctor(object, native int)
        IL_001b: newobj instance void [mscorlib]System.Threading.Thread::.ctor(class [mscorlib]System.Threading.ThreadStart)
        IL_0020: call instance void [mscorlib]System.Threading.Thread::Start()
        IL_0025: ldloc.0
        IL_0026: ldfld int32 ConsoleApplication2.ThreadTest/'<>c__DisplayClass0_0'::i
        IL_002b: ldc.i4.1
        IL_002c: add
        IL_002d: stloc.1
        IL_002e: ldloc.0
        IL_002f: ldloc.1
        IL_0030: stfld int32 ConsoleApplication2.ThreadTest/'<>c__DisplayClass0_0'::i

        IL_0035: ldloc.0
        IL_0036: ldfld int32 ConsoleApplication2.ThreadTest/'<>c__DisplayClass0_0'::i
        IL_003b: ldc.i4.s 10
        IL_003d: blt.s IL_000f
    // end loop

    IL_003f: ret
} // end of method ThreadTest::Main

但是最奇怪的事情是,如果我像这样更改原始代码,用i = i + 1替换i++

class ThreadTest
{
    static void Main(string[] args)
    {
        for (int i = 0; i < 10; i = i + 1)
            new Thread(() => Console.WriteLine(i)).Start();
    }
}

我知道您需要的是什么:

internal class ThreadTest
{
    private static void Main(string[] args)
    {
        int i;
        for (i = 0; i < 10; i++)
        {
            new Thread(delegate
            {
                Console.WriteLine(i);
            }).Start();
        }
    }
}

这正是我所期望的。

以下是字节码:

.method private hidebysig static 
    void Main (
        string[] args
    ) cil managed 
{
    // Method begins at RVA 0x2050
    // Code size 62 (0x3e)
    .maxstack 3
    .entrypoint
    .locals init (
        [0] class ConsoleApplication2.ThreadTest/'<>c__DisplayClass0_0' 'CS$<>8__locals0'
    )

    IL_0000: newobj instance void ConsoleApplication2.ThreadTest/'<>c__DisplayClass0_0'::.ctor()
    IL_0005: stloc.0
    IL_0006: ldloc.0
    IL_0007: ldc.i4.0
    IL_0008: stfld int32 ConsoleApplication2.ThreadTest/'<>c__DisplayClass0_0'::i
    IL_000d: br.s IL_0033
    // loop start (head: IL_0033)
        IL_000f: ldloc.0
        IL_0010: ldftn instance void ConsoleApplication2.ThreadTest/'<>c__DisplayClass0_0'::'<Main>b__0'()
        IL_0016: newobj instance void [mscorlib]System.Threading.ThreadStart::.ctor(object, native int)
        IL_001b: newobj instance void [mscorlib]System.Threading.Thread::.ctor(class [mscorlib]System.Threading.ThreadStart)
        IL_0020: call instance void [mscorlib]System.Threading.Thread::Start()
        IL_0025: ldloc.0
        IL_0026: ldloc.0
        IL_0027: ldfld int32 ConsoleApplication2.ThreadTest/'<>c__DisplayClass0_0'::i
        IL_002c: ldc.i4.1
        IL_002d: add
        IL_002e: stfld int32 ConsoleApplication2.ThreadTest/'<>c__DisplayClass0_0'::i

        IL_0033: ldloc.0
        IL_0034: ldfld int32 ConsoleApplication2.ThreadTest/'<>c__DisplayClass0_0'::i
        IL_0039: ldc.i4.s 10
        IL_003b: blt.s IL_000f
    // end loop

    IL_003d: ret
} // end of method ThreadTest::Main

为什么编译器在第一个场景中添加了j

注意:我正在使用VS 2015 Update 3,.NET Framework 4.5.2,在发布模式下编译。


对于 for (i = 0; i < 10; ++i) 做同样的事情。 - wake-0
你是如何“反汇编”代码的? - mjwills
1
尝试使用++i而不是i++,看看会发生什么。++i很像i = i + 1,而i++被定义为增加i但返回它在增加之前的值。当然,由于您正在丢弃i++i = i + 1的结果,除了额外生成的IL代码(可能会被JIT编译器删除)外,这没有任何区别,但如果您正在使用此表达式的结果,则它们将是两个不同的事物。 - Michael Geary
你为什么认为编译器在第一个场景中添加了“j”?编译器生成IL代码。你怎么确定是编译器出现了“错误”,而不是ILSpy? - mjwills
@mjwills 这不是错误,对吧?代码并没有错误,只是没有被优化。看起来 C# 编译器将 i++ 解释为后置自增运算符,因此生成的代码会在递增之前保存值 - 即使该值最终将被丢弃。它留给 JIT 编译器来清理和优化它。另一方面,你可能是对的,ILSpy 可能正在做一些有趣的事情!直接查看字节码而不是反编译将揭示真正发生的事情。 - Michael Geary
显示剩余6条评论
2个回答

4
因为从语义上讲,当你写i++时,编译器需要保留i的原始值,以便将其用作表达式的结果值。编译器通过引入一个新变量来实现这一点,新值可以保存在其中,直到需要使用i的旧值。因此,在将更新后的j值复制到i之前,旧值i仍然可供读取。当然,在这种情况下,由于确实没有代码需要该值,所以在将add指令的结果复制到j之后立即发生了这种情况。但是,对于i的值而言,它的旧值仍然存在,如果需要的话,就可以使用它。
你可能会问道:

但是,我从未使用过那个值。为什么编译器要保留它?为什么不直接将add的结果写入i,而是先将其存储在j中?

C#编译器不负责优化。它的主要工作是将C#代码转换为IL。事实上,我会说,这项工作的一部分是不要过度努力地优化事物,而是遵循常见的实现模式,以使JIT编译器更容易优化。通过不包括优化这种退化场景的逻辑,可以更轻松地确保C#编译器正在生成正确的IL,并以可预测、更易于优化的方式进行。

"C#编译器不负责优化。" 从C++背景来看,这正是我所缺失的部分。谢谢。 - themiurge

0

i++并不完全等同于i = i + 1,因为你也可以这样做:

尝试运行以下代码:

int i = 1;
int x = 5 + i++;
Console.WriteLine("i:" + i + " x: " + x);
i = 1;
int y = 5 + ++i;
Console.WriteLine("i:" + i + " y: " + y);

输出:

i:2 x: 6
i:2 y: 7

这与前缀和后缀的递增/递减有关(请参见前缀(++x)和后缀(x++)操作如何工作?)。

我知道前缀/后缀递增是如何工作的,这显然不是问题所在。 - themiurge
3
可以的。看起来 C# 编译器生成了“愚蠢”的 IL 代码,假设 JIT 编译器会清理它。在这种情况下,i++++i 会执行相同的操作,因为结果被丢弃,但也许编译器不够聪明以侦测这一点,只是把它留给 JIT 编译器处理。 - Michael Geary
@MichaelGeary:这就是我需要的澄清,谢谢。 - themiurge
如果你没有查看编译器的输出,那么猜测编译器在做什么是毫无意义的。例如,如果我将英语翻译成法语(即“编译”),然后你将我的法语翻译成英语(通过“ilspy”反编译),最后有人查看了最终的英语版本,那么他们对我的法语翻译就无法做出任何有意义的评论,因为他们从未看过它! - mjwills
@mjwills:我在我的问题中发布了字节码,请看一下。 - themiurge

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接