导致 C# 中过多的匿名方法闭包 (c__DisplayClass1) 的原因



Eqatec 显示每次调用一个在我的程序中包含简单 LINQ 'Where' 语句的方法时,都会调用数千个匿名方法闭包。伪代码示例:

Class1
{
    //foo and bar are both EF model classes
    List<foo> aList; // n = 2000
    List<bar> bList; // n = ~4000
    void aMethod() 
    {  
        foreach (var item in aList)
        {
            Class2.DoSomeWork(item, bList);
        }
    }
}
Class2
{
    static void DoSomeWork(foo item, List<bar> bList)
    {
     var query = bList.where(x => x.prop1 == item.A && x.prop2 = item.B).toList(); // <--- Calls thousands of anonymous method closures each method call.
     if (query.any()) <--- Calls only 1 anonymous method closure.
        DoSomethingElse(); 
    } 
}

我不明白为什么 2,000 次调用"DoSomeWork"调用了大约 800 万个匿名方法闭包(即使是 1 个也会导致数千个)。

作为修复,我只是重写了语句而不使用 LINQ,这消除了对闭包的需求,并使性能提高了 10 倍。

我仍然想了解为什么首先会发生这种情况,如果有人有一些他们想分享的理论。

我认为 8M 指的是在闭包类上执行方法的次数,而不是创建的闭包实例数。首先,让我们编译代码:

class Class2
{
    public static void DoSomeWork(foo item, List<bar> bList)
    {
        var query = bList.Where(x => x.prop1 == item.A && x.prop2 == item.B)
                         .ToList();
        if (query.Any())
            DoSomethingElse();
    }
    static void DoSomethingElse() { }
}
class foo { public int A { get; set; } public int B { get; set; } }
class bar { public int prop1 { get; set; } public int prop2 { get; set; } }

现在,我们可以丢弃原来的"//<--- 只调用 1 个匿名方法闭包"注释,因为实际上.Any()没有使用匿名方法闭包 - 它只是检查列表是否有内容:不需要闭包。

现在;让我们手动重写闭包以显示编译器中发生的情况:

class Class2
{
    class ClosureClass
    {
        public foo item; // yes I'm a public field
        public bool Predicate(bar x)
        {
            return x.prop1 == item.A && x.prop2 == item.B;
        }
    }
    public static void DoSomeWork(foo item, List<bar> bList)
    {
        var ctx = new ClosureClass { item = item };
        var query = bList.Where(ctx.Predicate).ToList();
        if (query.Any()) {
            DoSomethingElse();
        }
    }
    static void DoSomethingElse() { }
}

你可以看到每DoSomeWork创建1个ClosureClass,这直接映射到唯一捕获的变量(item)在方法级别的作用域。谓词ctx.Predicate) 只获取一次,但对 bList 中的每个项目都调用。所以确实,2000 * 4000 是对方法的 8M 调用;但是,对方法的 8M 调用不一定很慢。

然而!我认为最大的问题是你正在创建一个新列表只是为了检查是否存在。你不需要那个。您可以通过更早地移动Any来提高代码的效率:

if (bList.Any(x => x.prop1 == item.A && x.prop2 == item.B)) {
    DoSomethingElse();
}

现在,这只会在找到匹配项之前调用谓词足够的次数,我们应该预期该次数少于所有匹配项;它也不会不必要地填充列表。

现在;是的,手动执行此操作会更有效一些,即

bool haveMatch = false;
foreach(var x in bList) {
    if(x.prop1 == item.A && x.prop2 == item.B) {
        haveMatch = true;
        break;
    }
}
if(haveMatch) {
    DoSomethingElse();
}

但请注意,Anyforeach之间的这种变化并不是关键的区别;关键的区别是我删除了ToList()和"继续阅读,即使你已经找到了匹配项"。Any(predicate)用法更加简洁,易于阅读等。这通常不是性能问题,我怀疑它在这里。

在行中

var query = bList.where(x => x.prop1 == item.A && x.prop2 = item.B).toList();

bList 有 4000 个元素,x => x.prop1 == item.A && x.prop2 = item.B将被调用 4000 次。如果要延迟计算.Any(),请删除.ToList()

最新更新