C# 中用于捕获委托的循环反汇编的无用变量?
Useless variable in C# for loop disassembly with capturing delegate?
我试着看一下 this old question 中发布的代码的反汇编,我发现了一些奇怪的东西。
为了清楚起见,这里是源代码:
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 = i + 1
:
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,在发布模式下编译。
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
这与前缀和后缀有关 increment/decrement(参见 How do Prefix (++x) and Postfix (x++) operations work?)。
因为在语义上,当你写 i++
时,编译器需要保留 i
的原始值,以便它可以用作表达式的结果值。
编译器通过引入一个新变量来实现这一点,如果需要,可以在其中保留新值,直到使用 i
中的旧值。因此,旧值 i
仍然可以读取,直到更新的 j
值被复制到 i
中。当然,在这种情况下,在将 add
指令的结果复制到 j
之后会立即发生,因为实际上没有代码需要该值。但是,暂时i
的价值仍然是旧的,如果需要的话可以使用。
你可能会争论:
But, I never use that value. Why does the compiler keep it? Why not just write the result of the add
directly into i
instead of storing it in j
first?
C# 编译器不负责优化。它的主要工作是将 C# 代码翻译成 IL。事实上,我想说这项工作的一部分是 而不是 非常努力地优化事物,而是遵循通用的实现模式,使 JIT 编译器上的事情变得更容易,这负责优化。
通过不包括优化这种退化场景的逻辑,可以更轻松地确保 C# 编译器生成正确的 IL,并以可预测、更易于优化的方式进行生成。
我试着看一下 this old question 中发布的代码的反汇编,我发现了一些奇怪的东西。
为了清楚起见,这里是源代码:
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 = i + 1
:
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,在发布模式下编译。
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
这与前缀和后缀有关 increment/decrement(参见 How do Prefix (++x) and Postfix (x++) operations work?)。
因为在语义上,当你写 i++
时,编译器需要保留 i
的原始值,以便它可以用作表达式的结果值。
编译器通过引入一个新变量来实现这一点,如果需要,可以在其中保留新值,直到使用 i
中的旧值。因此,旧值 i
仍然可以读取,直到更新的 j
值被复制到 i
中。当然,在这种情况下,在将 add
指令的结果复制到 j
之后会立即发生,因为实际上没有代码需要该值。但是,暂时i
的价值仍然是旧的,如果需要的话可以使用。
你可能会争论:
But, I never use that value. Why does the compiler keep it? Why not just write the result of the
add
directly intoi
instead of storing it inj
first?
C# 编译器不负责优化。它的主要工作是将 C# 代码翻译成 IL。事实上,我想说这项工作的一部分是 而不是 非常努力地优化事物,而是遵循通用的实现模式,使 JIT 编译器上的事情变得更容易,这负责优化。
通过不包括优化这种退化场景的逻辑,可以更轻松地确保 C# 编译器生成正确的 IL,并以可预测、更易于优化的方式进行生成。