这里保存的是什么分配?

8
在 Filip Ekberg 的《.NET 异步编程入门》一书中,在“异步编程深入探讨/使用附加和分离的任务”一章中,他说通过在异步匿名方法中使用 service 值,引入了一个闭包和不必要的分配

enter image description here

接下来,他说最好将services作为参数传递给StartNew方法的操作委托,这样可以避免闭包而避免不必要的分配。

enter image description here

我的问题是: 作者说通过将services作为参数传递,可以实现什么样的分配?

为了更方便地进行调查,我选取了一个简单得多的例子,并将其放在了sharplab.io中:

  1. Not passing the parameter:

    using System;
    
    public class C {
        public void M() {
            var i = 1;     
            Func<int> f = () => i + 1;
            f();
        }
    }
    

    which compiles to:

    public class C
    {
        [CompilerGenerated]
        private sealed class <>c__DisplayClass0_0
        {
            public int i;
    
            internal int <M>b__0()
            {
                return i + 1;
            }
        }
    
        public void M()
        {
            <>c__DisplayClass0_0 <>c__DisplayClass0_ = new <>c__DisplayClass0_0();
            <>c__DisplayClass0_.i = 1;
            new Func<int>(<>c__DisplayClass0_.<M>b__0)();
        }
    }
    

    There are two allocations, one for the generated class and one for the Func<int> delegate.

  2. Passing the parameter:

    using System;
    
    public class C {
        public void M() {
            var i = 1;        
            Func<int, int> f = (x) => x + 1;
            f(i);
        }
    }
    

    which compiles to:

    public class C
    {
        [Serializable]
        [CompilerGenerated]
        private sealed class <>c
        {
            public static readonly <>c <>9 = new <>c();
    
            public static Func<int, int> <>9__0_0;
    
            internal int <M>b__0_0(int x)
            {
                return x + 1;
            }
        }
    
        public void M()
        {
            int arg = 1;
            (<>c.<>9__0_0 ?? (<>c.<>9__0_0 = new Func<int, int>(<>c.<>9.<M>b__0_0)))(arg);
        }
    }
    

    Here, there are still two allocations, one for the generated class (note there's a static field in the class which creates an instance of the class) and another allocation for the Func<int> delegate.

据我所见,在这两种情况下,编译器都会生成一个类并进行两次分配。唯一的区别在于,在第一种情况下,生成的类具有一个成员变量:
[CompilerGenerated]
private sealed class <>c__DisplayClass0_0
{
    public int i;

这确实会增加分配的大小,因为生成的类占用的内存比第二种情况要多。

我理解得对吗?这是作者所指的吗?


1
“是”是一个无聊的答案。术语有点不对,需要捕获服务/变量,将其作为参数传递可以避免这种情况。这是一种微观优化,在异步代码中你永远不会注意到差异。 - Hans Passant
@HansPassant 我认为任何答案都可以,我只是想确保我的推理是正确的。任何额外的信息也可能有所帮助。谢谢 - Don Box
@HansPassant 我理解“捕获”的概念。但如果我的推理是正确的,这个“捕获”确实意味着更多的分配。我想这就是作者的意思。 - Don Box
方法参数也需要分配内存。但是捕获的参数存储在GC堆上,而参数存储在堆栈或处理器寄存器上。虽然这是微小的优化,但你必须非常不幸才能将其提升到第二代,并且没有人可以调试那种代码,其中你会得到超过一千字节的代码。最好将其作为样式偏好,我从来不太喜欢那些悬挂的参数。 - Hans Passant
当我说“分配”时,我指的是堆分配。当然,栈分配仍然会发生,因为它涉及调用方法。但据我所知,我们不太关心栈分配,而是非常昂贵的堆分配。 - Don Box
1个回答

3

区别在于缓存。

在您的原始代码中,M() 每次调用时都会创建一个新的委托实例。在“聪明”的版本中,只创建了一个实例,并将其存储在静态变量中。

因此,如果您仅调用一次M(),则将分配相同数量的对象。如果您调用 M() 一百万次,使用第一个代码将比使用第二个代码分配更多的对象。

以下是这段代码:

(<>c.<>9__0_0 ?? (<>c.<>9__0_0 = new Func<int, int>(<>c.<>9.<M>b__0_0)))(arg);

...应该被有效地阅读为:

if (cachedDelegate == null)
{
    cachedDelegate = new Func<int, int>(GeneratedClass.CachedInstance.Method);
}
cachedDelegate.Invoke(arg);
< p > <>c 的实例也被缓存(上面提到的称为 GeneratedClass.CachedInstance)- 只创建了一个单独的实例。(尽管在这里这个不是那么重要,因为它只需要在委托被创建时创建...我不确定在什么情况下编译器优化特别有用。)


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