LINQ查询很慢

时间:2012-10-10 12:51:08

标签: c# .net performance linq

在应用程序分析期间,我发现模式匹配的函数检查非常慢。它是使用LINQ编写的。用循环简单替换这个LINQ表达式会产生巨大的差异。它是什么? LINQ真的是一件坏事吗?工作得太慢或我误解了什么?

private static bool PatternMatch1(byte[] buffer, int position, string pattern)
{
    int i = 0;

    foreach (char c in pattern)
    {
        if (buffer[position + i++] != c)
        {
            return false;
        }
    }

    return true;
}    
带有LINQ的

版本2(由Resharper建议)

private static bool PatternMatch2(byte[] buffer, int position, string pattern)
{
    int i = 0;
    return pattern.All(c => buffer[position + i++] == c);
}
带有LINQ的

版本3

private static bool PatternMatch3(byte[] buffer, int position, string pattern)
{
    return !pattern.Where((t, i) => buffer[position + i] != t).Any();
}

版本4使用lambda

private static bool PatternMatch4(byte[] buffer, int position, string pattern, Func<char, byte, bool> predicate)
{
    int i = 0;

    foreach (char c in pattern)
    {
        if (predicate(c, buffer[position + i++]))
        {
            return false;
        }
    }

    return true;
}

以下是大缓冲区的用法

const int SIZE = 1024 * 1024 * 50;

byte[] buffer = new byte[SIZE];

for (int i = 0; i < SIZE - 3; ++i)
{
    if (PatternMatch1(buffer, i, "xxx"))
    {
        Console.WriteLine(i);
    }
}

致电PatternMatch2PatternMatch3非常缓慢。 PatternMatch3大约需要8秒,PatternMatch2需要大约4秒,而PatternMatch1大约需要0.6。据我所知,它是相同的代码,我认为没有区别。有什么想法吗?

4 个答案:

答案 0 :(得分:7)

Mark Byers和Marco Mp对于额外的呼叫开销是正确的。但是,这里还有另一个原因:由于闭包,许多对象分配。

编译器在每次迭代时创建一个新对象,存储谓词可以使用的bufferpositioni的当前值。这是PatternMatch2的dotTrace快照,显示了这一点。 <>c_DisplayClass1是编译器生成的类。

(请注意,这些数字很大,因为我正在使用跟踪性能分析,这会增加开销。但是,每次调用的开销是相同的,因此总体百分比是正确的。)

enter image description here

答案 1 :(得分:6)

不同之处在于LINQ版本具有额外的函数调用。在LINQ内部,你的lambda函数在循环中被调用。

虽然额外的呼叫可能JIT compiler优化掉了,但不能保证它可以增加很小的开销。在大多数情况下,额外的开销并不重要,但由于您的功能非常简单并且被称为极其多次,即使很小的开销也可以快速累加。根据我的经验,LINQ代码通常比等效的for循环慢一点。这是您经常为更高级语法支付的性能价格。

在这种情况下,我建议坚持使用显式循环。

在优化此部分代码时,您可能还需要考虑更有效的搜索算法。您的算法最差情况为O(n * m)但better algorithms exist

答案 2 :(得分:6)

好吧,让我们选择Where运营商。

它的实现几乎是(*),如:

public IEnumerable<T> Where(this IEnumerable<T> input, Func<T, bool> fn)
{
    foreach(T i in input) 
        if (fn(i))
            yield return i;
}

这意味着,对于IEnumerable上的每个循环,都会创建一个迭代器对象 - 请注意,您有这些分配的SIZE-n,因为您执行了那么多LINQ查询。

然后对于模式中的每个角色:

  1. 对枚举器的函数调用
  2. 对谓词的函数调用
  3. 第二个是对代表的调用,其成本大约是典型虚拟调用的调用成本的两倍(在第一个版本中,除了数组去索引之外没有额外的调用。

    如果你真的想要粗暴的表现,你可能希望获得尽可能“老式​​”的代码。 在这种情况下,我甚至会用方法1替换方法1中的foreach(至少如果它不能用于优化,它会使它更具可读性,因为无论如何都要跟踪索引)。

    在这种情况下它也更具可读性,并且它表明Resharper建议有时是值得商榷的。

    (*)我说几乎是因为它使用代理方法来检查输入枚举是不是null并且在枚举集合之前抛出异常 - 这是一个小细节,它不会使我之前写的内容无效,在此强调完整性。

答案 3 :(得分:1)

LINQ应用于集合时的主要目标是它的简单性。如果你想要性能,你应该完全避免使用LINQ。同样要枚举一个数组,只需增加一个索引变量,而LINQ需要设置一个完整的枚举器对象。