【问题标题】:Why are there memory allocations when calling a func为什么调用函数时会有内存分配
【发布时间】:2018-08-24 06:19:27
【问题描述】:

我有以下程序,它从两个静态方法构造一个本地 Func。但奇怪的是,当我分析程序时,它分配了接近一百万个 Func 对象。为什么调用 Func 对象也会创建 Func 实例?

public static class Utils
{
    public static bool ComparerFunc(long thisTicks, long thatTicks)
    {
        return thisTicks < thatTicks;
    }
    public static int Foo(Guid[] guids, Func<long, long, bool> comparerFunc)
    {
        bool a = comparerFunc(1, 2);
        return 0;
    }
}
class Program
{
    static void Main(string[] args)
    {
        Func<Guid[], int> func = x => Utils.Foo(x, Utils.ComparerFunc);
        var guids = new Guid[10];
        for (int i = 0; i < 1000000; i++)
        {
            int a = func(guids);
        }
    }
}

【问题讨论】:

  • 那是因为它处于调试模式,编译器会在构建处于发布模式时优化代码
  • @EhsanSajjad:不,我不相信是因为这个。

标签: c# performance delegates garbage-collection closures


【解决方案1】:

您正在使用方法组转换来创建用于comparerFunc 参数的Func&lt;long, long, bool&gt;。不幸的是,C# 5 规范当前要求在每次运行时创建一个新的委托实例。来自 C# 5 规范第 6.6 节,描述了方法组转换的运行时评估:

分配了委托类型 D 的新实例。如果没有足够的可用内存来分配新实例,则会抛出 System.OutOfMemoryException 并且不会执行进一步的步骤。

匿名函数转换部分(6.5.1)包括:

允许(但不要求)将具有相同(可能为空)捕获的外部变量实例集的语义相同的匿名函数转换为相同的委托类型,以返回相同的委托实例。

...但是方法组转换没有类似的东西。

这意味着允许此代码被优化为对涉及的每个委托使用单个委托实例 - Roslyn 也这样做。

Func<Guid[], int> func = x => Utils.Foo(x, (a, b) => Utils.ComparerFunc(a, b));

另一种选择是分配一次Func&lt;long, long, bool&gt; 并将其存储在局部变量中。该局部变量需要被 lambda 表达式捕获,这会阻止 Func&lt;Guid[], int&gt; 被缓存 - 这意味着如果您多次执行 Main,您将在每次调用时创建两个新的委托,而早期的解决方案将在合理的范围内缓存。代码更简单:

Func<long, long, bool> comparer = Utils.ComparerFunc;
Func<Guid[], int> func = x => Utils.Foo(x, comparer);
var guids = new Guid[10];
for (int i = 0; i < 1000000; i++)
{
    int a = func(guids);
}

所有这一切都让我感到难过,在最新版本的 ECMA C# 标准中,编译器将被允许缓存方法组转换的结果。我不知道它何时/是否会这样做。

【讨论】:

  • @JonSkeet 我完全同意;如果它发生,这将是一个巨大的好处 - 在代码审查中,我经常看到(并修复)方法组以分配方式使用的情况,其中 lambda 化它(这是一个词吗? ) 没有 - 但是 a:我并不完美,并且 b:很多人不知道这一点(他们为什么必须知道?)。所以:如果你想一起去俱乐部,我们可以给 Jared 送一两杯啤酒,“完全不是贿赂的方式,诚实(但请让它发生,kthxbye)”:)
  • @buffjape:如果无法检测到优化,那很好。在这种情况下,它可检测到的,因为您可以看出正在按照规范创建多个委托实例。 “结果”的那一部分并没有保持不变,因此它不是纯粹的优化。
  • @JonSkeet 有一个级别的全程序优化可以揭示在这种情况下实际上没有任何东西依赖于comparerFunc 的身份。但我并不惊讶这没有发生。
  • @TavianBarnes:这绝对不是 C# 编译器的工作,IMO。 JIT 编译器可能可能会这样做 - 但我不清楚这是一个经常足以证明 JIT 时间成本合理的问题。
  • 我认为这里有一个未解决的问题可以解决这个问题:github.com/dotnet/roslyn/issues/5835
猜你喜欢
  • 1970-01-01
  • 2020-07-18
  • 1970-01-01
  • 2011-04-11
  • 1970-01-01
  • 2015-10-25
  • 2020-10-26
  • 1970-01-01
  • 2021-04-16
相关资源
最近更新 更多