为什么在调用func时会有内存分配

Xia*_* Ge 47 c# performance delegates closures garbage-collection

我有以下程序,它从两个静态方法构造一个本地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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

Jon*_*eet 52

您正在使用方法组转换来创建Func<long, long, bool>用于comparerFunc参数的方法.不幸的是,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));
Run Code Online (Sandbox Code Playgroud)

另一种选择是分配Func<long, long, bool>一次并将其存储在局部变量中.该局部变量需要由lambda表达式捕获,这可以防止Func<Guid[], int>缓存 - 这意味着如果你执行了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);
}
Run Code Online (Sandbox Code Playgroud)

所有这些让我很难过,在最新版的ECMA C#标准中,编译器将被允许缓存方法组转换的结果.我不知道什么时候/它是否这么做,但.

  • @JonSkeet我完全同意; 如果发生这将是一个巨大的好处 - 在代码审查中我经常看到*(和修复)情况,其中方法组以分配的方式使用,其中lambda-itzing它(是一个单词?)不 - 但是答:我并不完美,而且b:很多人都不知道这一点(他们为什么要这么做?).所以:如果你想和我们一起俱乐部,我们可以给Jared送一两杯啤酒"完全没有贿赂,诚实(但请让它成为kthxbye)":) (4认同)
  • @buffjape:如果无法检测到优化,那就没问题了.在这种情况下,它*是*可检测的,因为您可以告诉您正在根据规范创建多个委托实例."结果"的那部分不是一样的,所以它不是纯粹的优化. (4认同)
  • @JonSkeet有一个完整的程序优化级别可以揭示在这种情况下实际上没有任何东西取决于`comparerFunc`的身份.但是,我并不感到惊讶. (2认同)
  • @TavianBarnes:绝对不是C#编译器的工作,IMO。JIT编译器*可能*可以这样做-但是我不清楚这是一个经常足以证明JIT时间成本合理的问题。 (2认同)
  • 我认为这里有一个待解决的问题:https://github.com/dotnet/roslyn/issues/5835 (2认同)