为什么这个 lambda 闭包虽然没有在运行时执行,但会产生垃圾?
Why does this lambda closure generate garbage although it is not executed at runtime?
我注意到以下代码会生成堆分配,这会在某些时候触发垃圾收集器,我想知道为什么会这样以及如何避免这种情况:
private Dictionary<Type, Action> actionTable = new Dictionary<Type, Action>();
private void Update(int num)
{
Action action;
// if (!actionTable.TryGetValue(typeof(int), out action))
if (false)
{
action = () => Debug.Log(num);
actionTable.Add(typeof(int), action);
}
action?.Invoke();
}
我知道使用 () => Debug.Log(num)
这样的 lambda 会生成一个小帮手 class(例如 <>c__DisplayClass7_0)来保存局部变量。这就是为什么我想测试是否可以将此分配缓存在字典中。但是,我注意到,即使由于 if 语句而从未达到 lambda 代码,对 Update 的调用也会导致分配。当我注释掉 lambda 时,分配从分析器中消失了。我正在使用 Unity Profiler 工具(Unity 游戏引擎中的性能报告工具),它在 development/debug 模式下以每帧字节数显示此类分配。
我推测编译器或 JIT 编译器会为方法范围内的 lambda 生成助手 class,尽管我不明白为什么需要这样做。
最后,有没有办法以这种方式缓存委托而不分配并且不强制调用代码提前缓存操作? (我知道,我也可以在客户端代码中分配一次操作,但在这个例子中,我非常想实现某种自动缓存,因为我没有对客户端的完全控制)。
免责声明:这主要是出于兴趣的理论问题。我确实意识到大多数应用程序不会从这样的微优化中受益。
I surmise that the compiler or JIT compiler generates the helper class for the lambda for the scope of the method even though I don't understand why this would be desirable.
考虑在同一方法中有多个带有闭包的匿名方法的情况(这种情况很常见)。您是想为每个实例创建一个新实例,还是让它们共享一个实例?他们选择了后者。两种方法各有利弊。
Finally, is there any way of caching delegates in this manner without allocating and without forcing the calling code to cache the action in advance?
只需将该匿名方法移动到它自己的方法中,以便当调用该方法时无条件地创建匿名方法。
private void Update(int num)
{
Action action = null;
// if (!actionTable.TryGetValue(typeof(int), out action))
if (false)
{
Action CreateAction()
{
return () => Debug.Log(num);
}
action = CreateAction();
actionTable.Add(typeof(int), action);
}
action?.Invoke();
}
(我没有检查分配是否发生在嵌套方法中。如果是,请将其设为非嵌套方法并传入 int。)
Servy 的回答是正确的,并且提供了一个很好的解决方法。我想我可以添加更多细节。
首先:C# 编译器的实现选择随时可能因任何原因发生变化;我在这里所说的只是语言的要求,您不应该依赖它。
如果你有一个 lambda 的封闭外层变量,那么所有封闭变量都会成为闭包 class 的字段,并且闭包 class 是从 long-一旦功能被激活,术语池 ("the heap")。 无论 是否从中读取闭包 class,都会发生这种情况。
编译器团队可以选择将闭包 class 的创建推迟到使用它的第一个点:读取或写入本地或创建委托的位置。但是,这会给该方法增加额外的复杂性!这使得方法更大,更慢,更可能发生缓存未命中,使抖动更难工作,它产生更多基本块,因此抖动可能会跳过优化,等等。这种优化可能不会为自己付出代价。
但是,编译器团队 确实 在更有可能获得回报的情况下进行类似的优化。两个例子:
- 迭代器块(其中包含
yield return
的方法)99.99% 可能的情况是 IEnumerable
将 GetEnumerator
调用 恰好一次。因此,生成的可枚举具有同时实现 IEnumerable
和 IEnumerator
的逻辑; 第一次 时间 GetEnumerator
被调用,对象被转换为 IEnumerator
并返回。在秒的时候,我们分配了第二个枚举器。这样在大概率场景下就省了一个对象,而且生成的额外代码也很简单,很少调用。
- 通常
async
方法有一个 "fast path" 而 returns 无需等待 - 例如,您可能第一次有一个昂贵的异步调用,然后结果被缓存并第二次返回。 C# 编译器生成的代码会在遇到第一个 await
之前避免创建 "state machine" 闭包,因此会阻止在快速路径上进行分配(如果有的话)。
这些优化往往会得到回报,但 99% 的时间当你有一个创建闭包的方法时,它实际上会创建闭包。真的不值得推迟。
我注意到以下代码会生成堆分配,这会在某些时候触发垃圾收集器,我想知道为什么会这样以及如何避免这种情况:
private Dictionary<Type, Action> actionTable = new Dictionary<Type, Action>();
private void Update(int num)
{
Action action;
// if (!actionTable.TryGetValue(typeof(int), out action))
if (false)
{
action = () => Debug.Log(num);
actionTable.Add(typeof(int), action);
}
action?.Invoke();
}
我知道使用 () => Debug.Log(num)
这样的 lambda 会生成一个小帮手 class(例如 <>c__DisplayClass7_0)来保存局部变量。这就是为什么我想测试是否可以将此分配缓存在字典中。但是,我注意到,即使由于 if 语句而从未达到 lambda 代码,对 Update 的调用也会导致分配。当我注释掉 lambda 时,分配从分析器中消失了。我正在使用 Unity Profiler 工具(Unity 游戏引擎中的性能报告工具),它在 development/debug 模式下以每帧字节数显示此类分配。
我推测编译器或 JIT 编译器会为方法范围内的 lambda 生成助手 class,尽管我不明白为什么需要这样做。
最后,有没有办法以这种方式缓存委托而不分配并且不强制调用代码提前缓存操作? (我知道,我也可以在客户端代码中分配一次操作,但在这个例子中,我非常想实现某种自动缓存,因为我没有对客户端的完全控制)。
免责声明:这主要是出于兴趣的理论问题。我确实意识到大多数应用程序不会从这样的微优化中受益。
I surmise that the compiler or JIT compiler generates the helper class for the lambda for the scope of the method even though I don't understand why this would be desirable.
考虑在同一方法中有多个带有闭包的匿名方法的情况(这种情况很常见)。您是想为每个实例创建一个新实例,还是让它们共享一个实例?他们选择了后者。两种方法各有利弊。
Finally, is there any way of caching delegates in this manner without allocating and without forcing the calling code to cache the action in advance?
只需将该匿名方法移动到它自己的方法中,以便当调用该方法时无条件地创建匿名方法。
private void Update(int num)
{
Action action = null;
// if (!actionTable.TryGetValue(typeof(int), out action))
if (false)
{
Action CreateAction()
{
return () => Debug.Log(num);
}
action = CreateAction();
actionTable.Add(typeof(int), action);
}
action?.Invoke();
}
(我没有检查分配是否发生在嵌套方法中。如果是,请将其设为非嵌套方法并传入 int。)
Servy 的回答是正确的,并且提供了一个很好的解决方法。我想我可以添加更多细节。
首先:C# 编译器的实现选择随时可能因任何原因发生变化;我在这里所说的只是语言的要求,您不应该依赖它。
如果你有一个 lambda 的封闭外层变量,那么所有封闭变量都会成为闭包 class 的字段,并且闭包 class 是从 long-一旦功能被激活,术语池 ("the heap")。 无论 是否从中读取闭包 class,都会发生这种情况。
编译器团队可以选择将闭包 class 的创建推迟到使用它的第一个点:读取或写入本地或创建委托的位置。但是,这会给该方法增加额外的复杂性!这使得方法更大,更慢,更可能发生缓存未命中,使抖动更难工作,它产生更多基本块,因此抖动可能会跳过优化,等等。这种优化可能不会为自己付出代价。
但是,编译器团队 确实 在更有可能获得回报的情况下进行类似的优化。两个例子:
- 迭代器块(其中包含
yield return
的方法)99.99% 可能的情况是IEnumerable
将GetEnumerator
调用 恰好一次。因此,生成的可枚举具有同时实现IEnumerable
和IEnumerator
的逻辑; 第一次 时间GetEnumerator
被调用,对象被转换为IEnumerator
并返回。在秒的时候,我们分配了第二个枚举器。这样在大概率场景下就省了一个对象,而且生成的额外代码也很简单,很少调用。 - 通常
async
方法有一个 "fast path" 而 returns 无需等待 - 例如,您可能第一次有一个昂贵的异步调用,然后结果被缓存并第二次返回。 C# 编译器生成的代码会在遇到第一个await
之前避免创建 "state machine" 闭包,因此会阻止在快速路径上进行分配(如果有的话)。
这些优化往往会得到回报,但 99% 的时间当你有一个创建闭包的方法时,它实际上会创建闭包。真的不值得推迟。