4

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(); 
    } 
}

我不明白为什么对“DoSomeWork”的 2,000 次调用调用了大约 800 万个匿名方法闭包(甚至 1 个导致数千个)。

作为修复,我只是简单地重写了语句而不使用 LINQ,它消除了对闭包的需要,并产生了 10 倍的性能提升。

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

4

2 回答 2

4

我认为 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() { }
}

您可以看到 1ClosureClass是创建的 per DoSomeWork,它直接映射到唯一捕获的变量 ( 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)用法更简洁,易于阅读等。这通常不是性能问题,我也怀疑它是否在这里。

于 2012-08-14T07:12:45.520 回答
3

在行

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().

于 2012-08-14T06:53:19.650 回答