方法开始时发生的C#闭包堆分配
我似乎遇到了C#编译器的一些奇怪行为 考虑以下代码示例:方法开始时发生的C#闭包堆分配,c#,.net,closures,roslyn,C#,.net,Closures,Roslyn,我似乎遇到了C#编译器的一些奇怪行为 考虑以下代码示例: static void Main(string[] args) { Foo(false, 8); } public static void Foo(bool execute, int x) { if (execute) { Task.Run(() => Console.WriteLine(x)); } } 运行此(在发行版中)显示发生了一些意外的分配。检查IL表明,闭包触发的堆分配
static void Main(string[] args)
{
Foo(false, 8);
}
public static void Foo(bool execute, int x)
{
if (execute)
{
Task.Run(() => Console.WriteLine(x));
}
}
运行此(在发行版中)显示发生了一些意外的分配。检查IL表明,闭包触发的堆分配出现在函数的最开始处,而不是在条件内:
.method public hidebysig static void
Foo(
bool execute,
int32 x
) cil managed
{
.maxstack 2
.locals init (
[0] class Test.Program/'<>c__DisplayClass1_0' 'CS$<>8__locals0'
)
IL_0000: newobj instance void Test.Program/'<>c__DisplayClass1_0'::.ctor()
IL_0005: stloc.0 // 'CS$<>8__locals0'
IL_0006: ldloc.0 // 'CS$<>8__locals0'
IL_0007: ldarg.1 // x
IL_0008: stfld int32 Test.Program/'<>c__DisplayClass1_0'::x
// [18 13 - 18 25]
IL_000d: ldarg.0 // execute
IL_000e: brfalse.s IL_0022
// [20 17 - 20 54]
IL_0010: ldloc.0 // 'CS$<>8__locals0'
IL_0011: ldftn instance void Test.Program/'<>c__DisplayClass1_0'::'<Foo>b__0'()
IL_0017: newobj instance void [mscorlib]System.Action::.ctor(object, native int)
IL_001c: call class [mscorlib]System.Threading.Tasks.Task [mscorlib]System.Threading.Tasks.Task::Run(class [mscorlib]System.Action)
IL_0021: pop
// [22 9 - 22 10]
IL_0022: ret
} // end of method Program::Foo
.method公共隐藏静态void
福(
布尔执行,
int32 x
)cil管理
{
.maxstack 2
.init(
[0]类测试。程序/'c_uudisplayClass1_0''CS$8_ulocals0'
)
IL_0000:newobj实例void Test.Program/'c_udisplayClass1_0':.ctor()
IL_0005:stloc.0/'CS$8__locals0'
IL_0006:ldloc.0/'CS$8__locals0'
IL_0007:ldarg.1//x
IL_0008:stfld int32 Test.Program/'c__DisplayClass1_0'::x
// [18 13 - 18 25]
IL_000d:ldarg.0//执行
ILU 000e:brfalse.s ILU 0022
// [20 17 - 20 54]
IL_0010:ldloc.0/'CS$8_ulocals0'
IL_0011:ldftn实例无效测试。程序/'c_显示类1_0'::'b_0'()
IL_0017:newobj实例无效[mscorlib]系统。操作::.ctor(对象,本机int)
IL_001c:调用类[mscorlib]System.Threading.Tasks.Task[mscorlib]System.Threading.Tasks.Task::Run(类[mscorlib]System.Action)
IL_0021:流行音乐
// [22 9 - 22 10]
IL_0022:ret
}//方法结束程序::Foo
我是不是遗漏了什么,有人能解释这种奇怪的行为吗?Roslyn是否可能生成为闭包分配的代码,而不管我们是否实际执行闭包?这种行为是出于设计 当方法具有闭包时,闭包内使用的所有变量都必须是闭包类的一部分(以便lambda可以访问其当前值) 如果编译器没有立即分配闭包,那么在创建闭包实例时,它必须将本地变量中的值复制到闭包类上的字段,这将浪费时间和内存
如果多个具有不同可达性(或者更糟糕的是,嵌套范围)的lambda靠近同一个变量,这也会使codegen的风险更大、更复杂。正如SLacks所述,这种行为是设计的,因为x是函数的一个参数 但是,分配可以“移动到”以下条件:
public static void Foo(bool execute, int x)
{
if (execute)
{
int localx = x;
Task.Run(() => Console.WriteLine(localx));
}
}
在这个特定场景中,转换是安全的,因为x在Foo的主体内和lambda中都没有修改。此外,if语句不是在循环中执行的(在这种情况下,转换实际上可能会增加分配的数量)。编译器不会为您进行分析,但您可以。闭包显然需要关闭它们使用的变量—我理解这一部分。我不明白的是,在方法开始时会发生什么操作,不能推迟到以后,当实际使用该方法时…局部变量值似乎也被复制到了方法开始时的代码中,所以我不明白你在第二句话中指的是什么样的额外副本。将值从本地复制到闭包类不仅代价高昂,而且在语义上也不正确。闭包的思想是关闭变量,而不是值,因此如果复制值,则不会观察到闭包外部局部变量的更改,也不会观察到闭包中所做的更改。@ShayRojansky:否;局部变量只存在于闭包中,因此没有可复制的内容(参数除外);如果在
if
之后修改x
,codegen将需要检查闭包是否存在,以找出要写入的内容,这很可怕。“发生了一些意外的分配”-也可能意味着预期是错误的。谢谢,是的,这就是我最终写东西的原因。不过,我最惊讶的是编译器没有为我这样做,我想知道是否有理由阻止它这样做。毕竟,它执行词法分析是为了知道变量定义的最高词法范围(在我的例子中是方法体,因为x是一个参数),它还可以检测变量使用的最高词法范围并在那里分配。@ShayRojansky编译器不需要理由不做某事。事实上,情况正好相反:默认情况下,功能未实现。请看,这种回答并没有多大帮助。我要问的是(显然)是否有充分的理由不实施这种优化,这种优化在某些情况下会减少堆分配,而不会立即产生明显的负面影响。这可能是因为该功能的复杂性无法保证性能的提高,或者是我没有考虑过的其他问题(这就是我询问社区的原因)。如果它不存在的唯一原因是没有人这样做,那么它可能会成为Roslyn的一个好问题。那么为什么不实施它,并在现实世界的程序中衡量性能提升?我相信你会发现1)分析、开发和测试功能比你想象的要复杂;2) 在现实世界中,项目的好处比你想象的要少。好处如此之低的原因之一是,您可以轻松地自己进行优化,而不会显著降低代码的可读性。很多事情使它变得复杂。例如,如果这不是一个if
语句,而是一个while
循环(或循环中的if),那么相同的转换可能会使性能更差。您对循环的评论就是我想要的答案,谢谢您。。。