使用委托会产生垃圾吗?

8
我正在使用XNA为xbox360开发游戏。在Xbox上,与PC上的垃圾收集器相比,垃圾收集器表现非常糟糕,因此将产生的垃圾保持最少对于游戏的顺畅运行非常重要。
我记得曾经读过一篇文章,称调用委托会创建垃圾,但是现在我无论如何也找不到任何有关委托创建垃圾的参考资料。我是自己编造的吗?还是委托真的很麻烦?
如果委托很麻烦,那么建议一个解决方法可以获得额外的奖励分数。
public delegate T GetValue<T>(T value, T[] args);

public static T Transaction<T>(GetValue<T> calculate, ref T value, params T[] args) where T : class
{
    T newValue = calculate(value, args);
    return foo(newValue);
}

目前我的代码看起来有点像这样,我能想到的唯一解决委托问题的方法是传入一个继承了接口IValueCalculator的类,然后我可以在该接口上调用方法,但这并不是很简洁!


委托应该非常小,执行时不应该创建大量垃圾。如果它们创建垃圾,则每个委托应该只有一小撮字节。传递接口引用并在使用完后不使用它也会创建垃圾。 - CodingBarfield
4个回答

14
在桌面环境中,垃圾是非常廉价的。你需要担心的是你产生了多少非垃圾内容。记住垃圾回收器的工作原理:首先标记所有已知的对象,然后清除所有活动对象上的标记并压缩这些活动对象。昂贵的步骤是“取消标记活动对象”。销毁垃圾是便宜的;昂贵的是识别活动对象,这个成本取决于你拥有的活动对象数量(以及它们的引用拓扑复杂度),而不是死亡对象的数量。
但是,在XBOX和其他紧凑框架中,垃圾收集器会相当频繁地运行,并且在创建新分配时更频繁地运行,因此,确实需要担心创建垃圾。你希望保持活动集小(以便使收集廉价),并且不进行新分配(因为这触发了收集)。
创建委托确实会分配内存,但调用委托只是调用名为Invoke的类的方法。委托与具有在其被调用时立即调用另一个方法的Invoke方法的类别没有太大区别。
无论如何,如果您的内存性能存在问题,最好的做法是使用内存分析工具对程序进行分析。随意猜测是否这个或那个会分配内存,就像用指甲剪修剪花园一样费时间,而且实际上并没有达到您的目标。使用分析工具来分析性能并查看问题所在,然后解决它们。

我倾向于同意您的看法,而且我并不会过度担心垃圾回收问题——我只是想要了解情况!不过,您确定在 Xbox 上是这种情况吗?因为 Xbox 的垃圾回收器是非代际的。 - Martin
2
Xbox XNA GC的性能与活/死对象的数量有关,因此请尽量减少对象的总数。同时,请使用内存分析器来确定是否真的存在问题。 - Lasse V. Karlsen
实际上,在使用紧凑的.NET CLR的XBOX上,垃圾收集是很昂贵的,因为每隔几秒钟进行一次垃圾收集会破坏游戏玩法。在XNA XBOX开发中的常见做法是创建对象池或使用结构体代替类以避免进行垃圾收集。 - Olhovsky
@Olhovsky:你说得对,我会更新我的回答以澄清这一点。 - Eric Lippert

10

委托本身就是一个对象,因此如果您创建了一个委托(可能是匿名方法),并将其传递给其他方法执行,并且不把委托存储起来以备将来参考,则会产生垃圾。

例如:

collection.ForEach(delegate(T item)
{
    // do something with item
});

在这种情况下,将创建一个新的委托对象,但除了调用ForEach之外,它不会被引用,因此可以进行垃圾回收。
然而,仅仅是调用委托本身并不会产生垃圾,就像调用同一类型的任何其他方法一样。例如,如果您调用一个接受Object参数的委托,并传递一个Int32值,则该值将被装箱,但如果以相同方式调用普通方法,也会发生这种情况。
因此,使用委托应该没问题,但过度创建委托对象将是一个问题。
编辑:关于Xbox和XNA的内存管理的好文章在这里:Managed Code Performance on Xbox 360 for XNA: Part 2 - GC and Tools。请注意以下引用:
那么如何控制GC延迟?与设备的NetCF一样,Xbox GC也是非分代的。这意味着托管堆上的每个集合都是完整的集合。因此,我们发现GC延迟大约与活动对象的数量成正比…然后再加上堆压缩的成本。我们的基准测试显示,深层对象层次结构与浅层对象层次结构之间的差异微不足道,因此主要是对象的数量。小对象也往往比大对象更容易处理。
正如您所看到的,尽量避免创建大量不必要的对象,您就会得到更好的结果。

我明白了,那很好知道,尽管在没有创建任何委托的情况下尝试使用它们(除了在加载时)会很有趣。 - Martin
1
使用 C# 几乎会产生垃圾。我不建议避免使用短期引用。 - H H
4
通常我也不会这样做,但在Xbox上,使用XNA平台,如果你可以推迟垃圾回收(即减少其运行频率)而没有负面影响,那真是个好主意。因为如果它发生得太频繁,游戏就会变得卡顿不流畅。所以,如果想要解决这个问题,了解什么会导致垃圾回收以及何时进行垃圾回收都是一个好主意。例如,在我展示的代码中,如果你经常运行那个ForEach循环,或许你应该将委托存储在某个变量中,如果这不会改变行为的话。游戏优化通常与普通桌面DB应用程序不同。 - Lasse V. Karlsen
我知道这已经很老了,但是...如果您事先知道最终将为哪种方法创建委托(我假设是这种情况),那么请向拥有该方法的对象添加属性,该属性返回所需方法的委托。使用私有字段支持该属性,并在对象构造时或惰性地填充它。我相信您可以通过这种方式重复使用相同的委托实例(因此可以避免每次需要相关方法的委托时都创建新的委托),因为委托是引用对象(由编译器生成的类 - 正如其他人指出的那样)。 - AnorZaken

1

是和不是。

调用简单委托不会在堆上分配任何内容,但创建委托会在堆上分配64字节。

为了避免垃圾回收,可以预先创建委托。

让我们验证一下:

using BenchmarkDotNet.Running;

namespace Test
{
    class Program
    {
        static void Main(string[] args)
        {
            var summary = BenchmarkRunner.Run<BenchmarkDelegate>();            
        }
    }
}

基准测试:
using BenchmarkDotNet.Attributes;

namespace Test
{
    [MemoryDiagnoser]
    public class BenchmarkDelegate
    {
        public delegate int GetInteger();

        GetInteger _delegateInstance;

        public BenchmarkDelegate()
        {
            _delegateInstance = WithoutDelegate;
        }

        [Benchmark]
        public int WithInstance() => RunDelegated(_delegateInstance);

        [Benchmark]
        public int WithDelegate() => RunDelegated(WithoutDelegate);

        public int RunDelegated(GetInteger del) => del();

        [Benchmark]
        public int WithoutDelegate() => 0;
    }
}

以下是输出结果,向右滚动以查看“分配的内存/操作”列:
DefaultJob : .NET Core 2.2.1 (CoreCLR 4.6.27207.03, CoreFX 4.6.27207.03), 64bit RyuJIT
|          Method |       Mean |     Error |    StdDev | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op |
|---------------- |-----------:|----------:|----------:|------------:|------------:|------------:|--------------------:|
|    WithInstance |  7.5503 ns | 0.0751 ns | 0.0702 ns |           - |           - |           - |                   - |
|    WithDelegate | 35.4866 ns | 1.0094 ns | 1.2766 ns |      0.0203 |           - |           - |                64 B |
| WithoutDelegate |  0.0000 ns | 0.0000 ns | 0.0000 ns |           - |           - |

       - |                   - |

1

正如其他人已经指出的那样,委托创建会产生垃圾。

在您的示例中,使用 params 参数可能也会生成垃圾。

考虑提供不使用 params 关键字的重载。

这就是为什么库方法中通常存在有不同数量参数的重载以及使用 params 关键字的重载的原因:

请参见 String.Format Method (String, Object[])

Format Method (String, Object)
Format Method (String, Object[])
...
Format Method (String, Object, Object)
Format Method (String, Object, Object, Object)

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