为什么编译器不能通过内联来优化闭包变量?
Why can't the compiler optimize closure variable by inlining?
我有一个这样的Main
方法:
static void Main(string[] args)
{
var b = new byte[1024 * 1024];
Func<double> f = () =>
{
new Random().NextBytes(b);
return b.Cast<int>().Average();
};
var avg = f();
Console.WriteLine(avg);
}
由于我在此处访问局部变量 b
,因此编译器会创建一个 class 来捕获该变量,并且 b
成为该 class 的字段。然后 b
的寿命与编译器生成的 class 的寿命一样长,这会导致内存泄漏。即使 b
超出范围(可能不是在这种情况下,但可以想象这是在另一个方法内部而不是 Main
),字节数组也不会被释放。
我想知道的是,既然我在声明 Func
之后没有在任何地方访问或修改 b
,为什么编译器不能内联那个局部变量而不用创建一个 class?像这样:
Func<double> f = () =>
{
var b = new byte[1024 * 1024];
new Random().NextBytes(b);
return b.Cast<int>().Average();
};
我在 Debug 和 Release 模式下编译了这段代码,在这两种模式下都生成了 DisplayClass
:
这只是没有作为优化实现,还是我遗漏了什么?
Is this just not implemented as an optimization or is there anything I am missing?
对于您给出的 具体 示例,您可能不想进行该代码转换,因为它会改变程序的语义。如果 new
抛出异常,在原始程序中它应该在委托执行之前抛出异常,而在您的转换中,副作用会被延迟。这是否是一个应该保留的重要 属性 值得商榷。 (这样做也会给调试器带来问题;调试器已经必须假装闭包 类 的元素是包含方法体的局部变量,而这种优化可能会使它进一步复杂化。)
不过,更一般的一点是有关系的。如果您知道封闭变量仅用于其值,则可以进行许多优化。
当我在编译器团队时——我于 2012 年离开——我和 Neal Gafter 考虑实施此类优化,以及一些更复杂的优化,旨在降低昂贵对象生命周期被延长的可能性一不小心太长了
旁白:更复杂的场景中最简单的是:我们有两个 lambda 转换为委托;一个存储在一个短暂的变量中,并在包含对昂贵对象的引用的局部变量上关闭;一个存储在一个长期存在的变量中,并在引用廉价对象的局部变量上关闭。昂贵的对象与长寿命变量一样长,即使它没有被使用。更一般地,可以将多个闭包构造为基于封闭关系的分区;当时我们只根据嵌套来划分闭包;同一嵌套级别的闭包是一个闭包。给定的情况很少见,并且有明显的解决方法,但如果它根本没有发生就好了。
我们没有这样做是因为在我们实施 Roslyn 期间有更多重要的优化和功能,我们不想给已经很长的计划增加风险。
我们可以自信地执行此类优化,因为在 C# 中很容易知道何时为局部变量设置了别名,因此您可以确定它是否在创建闭包后被写入。
不知道这些优化有没有同时实现;可能不会。
我也不知道编译器是否对 C# 7 本地函数进行了此类优化,但我怀疑答案是 "yes"。试试本地函数,看看会发生什么!
我有一个这样的Main
方法:
static void Main(string[] args)
{
var b = new byte[1024 * 1024];
Func<double> f = () =>
{
new Random().NextBytes(b);
return b.Cast<int>().Average();
};
var avg = f();
Console.WriteLine(avg);
}
由于我在此处访问局部变量 b
,因此编译器会创建一个 class 来捕获该变量,并且 b
成为该 class 的字段。然后 b
的寿命与编译器生成的 class 的寿命一样长,这会导致内存泄漏。即使 b
超出范围(可能不是在这种情况下,但可以想象这是在另一个方法内部而不是 Main
),字节数组也不会被释放。
我想知道的是,既然我在声明 Func
之后没有在任何地方访问或修改 b
,为什么编译器不能内联那个局部变量而不用创建一个 class?像这样:
Func<double> f = () =>
{
var b = new byte[1024 * 1024];
new Random().NextBytes(b);
return b.Cast<int>().Average();
};
我在 Debug 和 Release 模式下编译了这段代码,在这两种模式下都生成了 DisplayClass
:
这只是没有作为优化实现,还是我遗漏了什么?
Is this just not implemented as an optimization or is there anything I am missing?
对于您给出的 具体 示例,您可能不想进行该代码转换,因为它会改变程序的语义。如果 new
抛出异常,在原始程序中它应该在委托执行之前抛出异常,而在您的转换中,副作用会被延迟。这是否是一个应该保留的重要 属性 值得商榷。 (这样做也会给调试器带来问题;调试器已经必须假装闭包 类 的元素是包含方法体的局部变量,而这种优化可能会使它进一步复杂化。)
不过,更一般的一点是有关系的。如果您知道封闭变量仅用于其值,则可以进行许多优化。
当我在编译器团队时——我于 2012 年离开——我和 Neal Gafter 考虑实施此类优化,以及一些更复杂的优化,旨在降低昂贵对象生命周期被延长的可能性一不小心太长了
旁白:更复杂的场景中最简单的是:我们有两个 lambda 转换为委托;一个存储在一个短暂的变量中,并在包含对昂贵对象的引用的局部变量上关闭;一个存储在一个长期存在的变量中,并在引用廉价对象的局部变量上关闭。昂贵的对象与长寿命变量一样长,即使它没有被使用。更一般地,可以将多个闭包构造为基于封闭关系的分区;当时我们只根据嵌套来划分闭包;同一嵌套级别的闭包是一个闭包。给定的情况很少见,并且有明显的解决方法,但如果它根本没有发生就好了。
我们没有这样做是因为在我们实施 Roslyn 期间有更多重要的优化和功能,我们不想给已经很长的计划增加风险。
我们可以自信地执行此类优化,因为在 C# 中很容易知道何时为局部变量设置了别名,因此您可以确定它是否在创建闭包后被写入。
不知道这些优化有没有同时实现;可能不会。
我也不知道编译器是否对 C# 7 本地函数进行了此类优化,但我怀疑答案是 "yes"。试试本地函数,看看会发生什么!