关于C#:应该避免使用linq,因为它很慢吗?

Should LINQ be avoided because it's slow?

我被告知,由于.NET LINQ速度太慢,我们不应该使用它,我想知道其他人也得出了同样的结论,例如:

花了1443毫秒做了100000000次比较。与Linq相比,花费了4944ms完成了100000000个任务。(慢243%)

非LINQ代码:

1
2
3
4
5
6
7
8
9
10
11
for (int i = 0; i < 10000; i++)
{
    foreach (MyLinqTestClass1 item in lst1) //100000 items in the list
    {
        if (item.Name =="9999")
        {
            isInGroup = true;
            break;
        }
    }
}

花了1443毫秒做了100000000次比较。

LINQ代码:

1
2
for (int i = 0; i < 10000; i++)  
    isInGroup = lst1.Cast<MyLinqTestClass1>().Any(item => item.Name =="9999");

与Linq相比,花费了4944ms完成了100000000个任务。

我猜优化LINQ代码是可能的,但我的想法是很容易得到非常慢的LINQ代码,而且不应该使用它。如果LINQ很慢,那么它也将遵循PLINQ很慢,NHibernate LINQ很慢,因此不应使用任何类型的LINQ语句。

有没有其他人发现Linq的速度如此之慢以至于他们希望自己从未使用过它,或者我是基于这样的基准得出了一个过于笼统的结论?


Should Linq be avoided because its slow?

不,如果不够快就应该避免。慢而不快根本不是一回事!

Slow与客户、管理层和利益相关者无关。不够快是非常重要的。永远不要衡量事物有多快;这不会告诉你什么你可以用来作为商业决策的基础。测量接近客户接受的程度。如果这是可以接受的,那么停止花钱让它更快,它已经足够好了。

性能优化是昂贵的。编写代码以便其他人能够阅读和维护它是很昂贵的。这些目标经常是相互对立的,所以为了负责任地花费利益相关者的钱,你必须确保你只花费宝贵的时间和精力在不够快的事情上进行性能优化。

您发现了一种人为的、不现实的基准情况,在这种情况下,LINQ代码的速度比其他编写代码的方法要慢。我向您保证,您的客户一点也不关心您不切实际的基准测试的速度。他们只关心你发送给他们的程序对他们来说是否太慢。我向你保证,你的管理层一点也不关心这个问题(如果他们有能力的话);他们关心你花费了多少不必要的钱来制造足够快的东西,使代码在这个过程中读、理解和维护起来更加昂贵。


你为什么用Cast()?基本上,您没有给我们足够的代码来真正判断基准。

是的,您可以使用LINQ编写慢代码。你猜怎么着?您也可以编写慢的非LINQ代码。

LINQ大大提高了处理数据的代码的表达能力…只要你花时间去理解LINQ,编写性能良好的代码并不难。

如果有人告诉我不要因为速度的原因而使用LINQ(尤其是对物体的LINQ),我会嘲笑他们。如果他们想出了一个具体的瓶颈,并说,"我们可以通过在这种情况下不使用LINQ来更快地实现这一目标,这就是证据",那么这是一个非常不同的问题。


也许我错过了什么,但我很确定你的基准已经偏离了。

我用以下方法测试:

  • Any扩展方法("linq")。
  • 一个简单的foreach循环(您的"优化"方法)
  • 使用ICollection.Contains方法
  • 使用优化数据结构的Any扩展方法(HashSet)

测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
class Program
{
    static void Main(string[] args)
    {
        var names = Enumerable.Range(1, 10000).Select(i => i.ToString()).ToList();
        var namesHash = new HashSet<string>(names);
        string testName ="9999";
        for (int i = 0; i < 10; i++)
        {
            Profiler.ReportRunningTimes(new Dictionary<string, Action>()
            {
                {"Enumerable.Any", () => ExecuteContains(names, testName, ContainsAny) },
                {"ICollection.Contains", () => ExecuteContains(names, testName, ContainsCollection) },
                {"Foreach Loop", () => ExecuteContains(names, testName, ContainsLoop) },
                {"HashSet", () => ExecuteContains(namesHash, testName, ContainsCollection) }
            },
            (s, ts) => Console.WriteLine("{0, 20}: {1}", s, ts), 10000);
            Console.WriteLine();
        }
        Console.ReadLine();
    }

    static bool ContainsAny(ICollection<string> names, string name)
    {
        return names.Any(s => s == name);
    }

    static bool ContainsCollection(ICollection<string> names, string name)
    {
        return names.Contains(name);
    }

    static bool ContainsLoop(ICollection<string> names, string name)
    {
        foreach (var currentName in names)
        {
            if (currentName == name)
                return true;
        }
        return false;
    }

    static void ExecuteContains(ICollection<string> names, string name,
        Func<ICollection<string>, string, bool> containsFunc)
    {
        if (containsFunc(names, name))
            Trace.WriteLine("Found element in list.");
    }
}

不要担心Profiler类的内部。它只是在一个循环中运行Action,并使用Stopwatch来计时。它还确保在每次测试之前调用GC.Collect(),以尽可能消除噪音。

结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
      Enumerable.Any: 00:00:03.4228475
ICollection.Contains: 00:00:01.5884240
        Foreach Loop: 00:00:03.0360391
             HashSet: 00:00:00.0016518

      Enumerable.Any: 00:00:03.4037930
ICollection.Contains: 00:00:01.5918984
        Foreach Loop: 00:00:03.0306881
             HashSet: 00:00:00.0010133

      Enumerable.Any: 00:00:03.4148203
ICollection.Contains: 00:00:01.5855388
        Foreach Loop: 00:00:03.0279685
             HashSet: 00:00:00.0010481

      Enumerable.Any: 00:00:03.4101247
ICollection.Contains: 00:00:01.5842384
        Foreach Loop: 00:00:03.0234608
             HashSet: 00:00:00.0010258

      Enumerable.Any: 00:00:03.4018359
ICollection.Contains: 00:00:01.5902487
        Foreach Loop: 00:00:03.0312421
             HashSet: 00:00:00.0010222

数据非常一致,说明了以下情况:

  • Na?使用Any扩展方法比na慢9%左右?使用foreach循环。

  • 使用最合适的方法(ICollection.Contains)和未优化的数据结构(List)比na快大约50%。使用foreach循环。

  • 使用一个优化的数据结构(HashSet)将性能方面的任何其他方法都彻底击垮。

我不知道你从哪里得到243%。我想这和所有的演员阵容有关。如果您使用的是ArrayList,那么不仅您使用的是未优化的数据结构,而且您使用的是一个非常过时的数据结构。

我可以预测接下来会发生什么。"是的,我知道您可以更好地优化它,但这只是一个比较Linq与非Linq性能的示例。"

是的,但是如果您甚至不能对您的示例进行彻底的说明,那么您怎么可能期望在生产代码中做到这样彻底呢?

底线是:

How you architect and design your software is exponentially more important than what specific tools you use and when.

如果您遇到性能瓶颈——这是LINQ与不使用LINQ时可能发生的每一点——那么就解决它们。埃里克关于自动性能测试的建议是一个很好的建议;这将帮助你尽早发现问题,以便你能够正确地解决它们——不是通过回避一个让你效率提高80%的令人惊奇的工具,而是通过实际调查问题并提出一个真正的解决方案。这可以提高你的表现2倍,10倍,100倍或更多。

创建高性能应用程序并不是要使用正确的库。它涉及到分析、做出好的设计选择和编写好的代码。


LINQ是否是一个现实世界的瓶颈(影响应用程序的整体性能还是感知性能)?

您的应用程序是否将对现实世界中超过100000000条记录执行此操作?如果是这样的话——那么你可能会考虑其他的选择——如果不是这样的话,那就像是说:"我们不能买这辆家庭轿车,因为它在180英里以上的时速下开得不好。"

如果它"只是慢",那么这不是一个很好的理由…通过这种推理,你应该在ASM/C/C++中编写所有的东西,而C"应该太慢了"。


虽然过早的pessimization(imho)和过早的优化一样糟糕,但是在不考虑使用环境的情况下,不应该排除基于绝对速度的整个技术。是的,如果您正在进行一些非常繁重的数字处理,而这是一个瓶颈,那么Linq可能会有问题——分析一下它。

您可以使用一个有利于LINQ的论点,那就是,虽然您可以用手写代码来超越它,但是LINQ版本可能更清晰、更易于维护,而且,与复杂的手动并行相比,PLINQ还有一个优势。


这种比较的问题在于它在抽象上毫无意义。

如果一开始用MyLinqTestClass1对象的name属性对其进行哈希运算,就可以击败这两个对象中的任何一个。如果可以按名称对它们进行排序,然后进行二进制搜索,则可以在这两种方法之间进行搜索。实际上,我们不需要为此存储myLinqTestClass1对象,只需要存储名称。

内存大小有问题吗?也许将名称存储在一个DAWG结构中,组合足够,然后将其用于此检查?

设置这些数据结构的额外开销是否有意义?不可能知道。

另一个问题是,Linq的概念有一个不同的问题,它的名字就是Linq。对于营销目的来说,微软能够说"这里有一堆很酷的新东西可以一起工作",这是很好的,但对于那些在做分析的时候把东西组合在一起的人来说,这样做会让他们分崩离析。你需要调用Any,它基本上实现了.net2.0天内常见的可枚举模式的过滤(对于.net1.1来说并不未知,尽管写起来更难,这意味着它只在某些情况下效率的好处很重要的地方使用),你有lambda表达式,并且你有所有的查询树都混在一起。在一个概念中。哪个是慢的?

我敢打赌这里的答案是lambda,而不是Any的使用,但我不会下太大的赌注(例如项目的成功),我会测试并确定。同时,lambda表达式与iqueryable一起工作的方式可以产生特别有效的代码,如果不使用lambda,以同等的效率编写代码将非常困难。

当Linq因为一个人工基准失败而擅长效率时,我们难道就不能变得高效吗?我不这么认为。

在有意义的地方使用LINQ。

在瓶颈条件下,尽管看起来是适当的或不适当的优化,但还是要转移到LINQ。首先不要写难以理解的代码,因为这样做会使真正的优化更加困难。


对我来说,这听起来像是你在写合同,雇主要么不理解Linq,要么不理解系统的性能瓶颈。如果使用GUI编写应用程序,那么使用LINQ对性能的影响可以忽略不计。在典型的GUI/Web应用程序中,内存调用占所有等待时间的不足1%。你,或者更确切地说是你的雇主,正在努力优化1%。这真的有益吗?

但是,如果您编写的应用程序是科学的,或者是高度面向数学的,很少有磁盘或数据库访问,那么我同意Linq并不是一种可行的方法。

顺便说一句,不需要石膏。以下功能等同于您的第一个测试:

1
2
       for (int i = 0; i < 10000; i++)
            isInGroup = lst1.Any(item => item.Name =="9999");

当我使用一个包含10000myLinqTestClass1对象的测试列表运行这个测试时,原始的运行时间为2.79秒,修改时间为3.43秒。在可能占用CPU时间不到1%的操作上节省30%不是很好地利用您的时间。


也许linq速度很慢,但是使用linq,我可以非常简单地并行代码。

这样地:

1
lst1.Cast<MyLinqTestClass1>().AsParallel().Any(item => item.Name =="9999");

如何将循环并行化?


"谁告诉我的?]因为.NET LINQ速度太慢了[为什么?]我们不应该用它"

根据我的经验,仅仅根据某人告诉你的事情来决定使用什么技术、库或语言是一个坏主意。

首先,这些信息来自你信任的来源吗?如果不是这样,您可能犯了一个巨大的错误,相信这个(可能是未知的)人来做您的设计决策。第二,这些信息在今天仍然相关吗?但好吧,基于简单而不太现实的基准,您已经得出结论,Linq比手动执行相同操作要慢。问自己一个自然的问题是:这段代码的性能至关重要吗?这段代码的性能是否会受到其他因素的限制,而不是我的LINQ查询的执行速度——比如数据库查询、等待I/O等?

以下是我喜欢工作的方式:

  • 确定要解决的问题,并根据您已经知道的需求和限制编写最简单的功能完整解决方案。
  • 确定您的实现是否真正满足需求(是否足够快?资源消耗是否保持在可接受的水平?).
  • 如果是这样,你就完了。如果没有,请寻找优化和优化解决方案的方法,直到它通过2的测试。在这里你可能需要考虑放弃一些东西,因为它太慢了。也许吧。不过,很有可能瓶颈根本不在您预期的位置。
  • 对我来说,这个简单的方法有一个单一的目的:通过减少我花在改进已经完全足够的代码上的时间来最大化我的生产力。

    是的,也许有一天你会发现原来的解决方案不再适用。或者可能不会。如果是这样的话,那就到处处理。我建议你不要浪费时间去解决假设的(未来的)问题。


    这是一个有趣的观察,因为你提到NHibernate是慢的,因为Linq是慢的。如果您正在执行LINQ to SQL(或NHibernate等效代码),那么您的LINQ代码将转换为SQL服务器上的一个exists查询,在该查询中,循环代码必须首先获取所有行,然后对它们进行迭代。现在,您可以轻松地编写这样一个测试,以便循环代码一次读取所有10 K运行的所有数据(单数据库查找),但LINQ代码实际上执行10 K SQL查询。这可能会显示出一个循环版本的巨大的速度优势,这在现实中并不存在。实际上,一个单独的exists查询每次都会比表扫描和循环更出色——即使您没有被查询列的索引(如果这个查询经常进行的话,您可能会这样做)。

    我并不是说你的测试就是这样的——我们没有足够的代码来查看——但可能是这样。也可能是linq-to对象的性能确实存在差异,但这可能根本无法转换为linq-to-sql。你需要知道你在测量什么,以及它对你现实世界的需求有多适用。


    是的,你是对的。用LINQ编写慢代码很容易。其他人也是对的:在没有linq的情况下,用c编写慢代码很容易。

    我在C语言中编写了和您相同的循环,它运行得更快一些毫秒。我从中得出的结论是C本身很慢。

    与LINQ->Loop扩展一样,在C中,执行相同操作所需的代码行数将是原来的5倍多,这会使编写速度变慢、更难阅读、更有可能出现错误,并且更难找到和修复错误,但如果每10亿次迭代节省几毫秒是很重要的,这通常是需要的。


    类型转换当然会减慢代码的速度。如果您非常关心,至少使用了一个强类型的IEnumerable进行比较。我自己尽可能使用LINQ。它使代码更加简洁。不必担心代码的必要细节。LINQ是一个功能性的概念,这意味着您将清楚地说明您想要发生什么,而不必担心如何发生。


    我宁愿说您应该避免过于努力地编写最有效的代码,除非它是强制的。


    正如您所演示的,可以编写性能优于LINQ代码的非LINQ代码。但反过来也是可能的。考虑到Linq可以提供的维护优势,您可能会考虑默认使用Linq,因为您不太可能遇到任何可以归因于Linq的性能瓶颈。

    也就是说,在某些情况下,Linq是行不通的。例如,如果要导入大量数据,可能会发现执行单个插入的操作比以XML批处理方式将数据发送到SQL Server慢。在本例中,不是linq插入比非linq插入快,而是不需要为批量数据导入执行单个SQL插入。


    Given that LINQ is slow then it would also follow that PLINQ is slow and NHibernate LINQ would be slow so any kind on LINQ statement should not be used.

    这是一种不同的环境,但却截然不同。当您谈论数据访问操作时,整个10亿操作的1.4秒与5秒是无关的。


    您的测试用例有点歪斜。any运算符将开始枚举结果,如果找到并退出,则在第一个实例中返回true。尝试使用简单的字符串列表来查看结果。要回答关于避免使用LINQ的问题,您应该真正过渡到使用LINQ。在执行复杂查询时,除了编译时检查外,它还使代码更容易读取。另外,您不需要在示例中使用cast操作符。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    string compareMe ="Success";
    string notEqual ="Not Success";

    List<string> headOfList = new List<string>();
    List<string> midOfList = new List<string>();
    List<string> endOfList = new List<string>();

    //Create a list of 999,999 items
    List<string> masterList = new List<string>();
    masterList.AddRange(Enumerable.Repeat(notEqual, 999999));

    //put the true case at the head of the list
    headOfList.Add(compareMe);
    headOfList.AddRange(masterList);

    //insert the true case in the middle of the list
    midOfList.AddRange(masterList);
    midOfList.Insert(masterList.Count/2, compareMe);

    //insert the true case at the tail of the list
    endOfList.AddRange(masterList);
    endOfList.Add(compareMe);


    Stopwatch stopWatch = new Stopwatch();

    stopWatch.Start();
    headOfList.Any(p=>p == compareMe);
    stopWatch.ElapsedMilliseconds.Dump();
    stopWatch.Reset();

    stopWatch.Start();
    midOfList.Any(p=>p == compareMe);
    stopWatch.ElapsedMilliseconds.Dump();
    stopWatch.Reset();

    stopWatch.Start();
    endOfList.Any(p=>p == compareMe);
    stopWatch.ElapsedMilliseconds.Dump();
    stopWatch.Stop();


    有上千倍更好的理由避免使用LINQ。

    以下引自对Linq的讨论,列举了其中的一些:

    QuoTe1

    "例如,这项工作:

    1
    2
    var a = new { x = 1, y = 2 };
    a = new { x = 1, y = 3 };

    但这不起作用:

    1
    2
    var a = new { x = 1, y = 2 };
    a = new { x = 1, y = 2147483649 };

    返回:Error 1 Cannot implicitly convert type 'AnonymousType#1' to 'AnonymousType#2'

    但这是可行的:

    1
    2
    var a = new { x = 1, y = 2147483648 };
    a = new { x = 1, y = 2147483649 };

    编译时:

    1
    var a = new { x = 1, y = 2 };

    x和y组件的类型被任意声明为32位有符号整数,它是平台所具有的众多整数类型之一,没有任何特殊之处。

    但还有更多。例如,这项工作:

    1
    2
    double x = 1.0;
    x = 1;

    但这不起作用:

    1
    2
    var a = new { x = 1.0, y = 0 };
    a = new { x = 1, y = 0 };

    数字转换规则不适用于此类类型。正如你所见,优雅无处不在。"

    QuoTe2

    "那么,似乎‘AnonymousType 1’和‘AnonymousType 2’不是同义词——它们命名不同的类型。由于{ x = 1, y = 2 }{ y = 2, x = 1 }分别是这两种类型的表达式,它们不仅表示不同的值,而且还表示不同类型的值。

    所以,我偏执是对的。现在,我的偏执狂进一步扩大,我不得不问Linq对以下比较的看法:

    1
    new { x = 1, y = 2 } == new { x = 1, y = 2 }

    结果为假,因为这是指针比较。

    但结果是:

    1
    (new { x = 1, y = 2 }).Equals(new { x = 1, y = 2 })

    是真的。

    结果是:

    1
    (new { x = 1, y = 2 }).Equals(new { y = 2, x = 1 })

    1
    (new { x = 1, y = 2 }).Equals(new { a = 1, b = 2 })

    是假的。"

    QuoTe3

    "更新面向记录:-o

    我同意,这是有问题的,并且来源于Linq的序列导向性。

    这对我来说是个表演障碍。如果我必须使用SQL进行更新,为什么还要为Linq操心呢?

    linq-to对象的优化是不存在的。

    没有任何代数优化和自动表达式重写。许多人不想使用linq-to对象,因为它们会损失很多性能。查询的执行方式与您编写查询的方式相同。"