LINQ是否应该被避免因为它很慢?

66

有人告诉我,由于.net的linq非常慢,我们不应该使用它,所以想知道是否还有其他人得出了相同的结论,例如:

进行1000000000次比较的非LINQ代码需要1443毫秒。
进行1000000000次比较的LINQ代码需要4944毫秒。
(慢了243%)

非LINQ代码:

for (int i = 0; i < 10000; i++)
{
    foreach (MyLinqTestClass1 item in lst1) //100000 items in the list
    {
        if (item.Name == "9999")
        {
            isInGroup = true;
            break;
        }
    }
}

在不使用LINQ的情况下,执行了1000000000次比较,共计耗时1443毫秒。

LINQ代码:

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

使用LINQ进行了1000000000次比较,用时4944毫秒。

我猜可能可以优化LINQ代码,但是想法是很容易写出非常慢的LINQ代码,考虑到它不应该被使用。鉴于LINQ很慢,那么PLINQ也会很慢,NHibernate LINQ也会很慢,因此任何种类的LINQ语句都不应该使用。

有没有其他人发现LINQ太慢,以至于他们希望从未使用过它,或者我是否基于这样的基准测试得出了一个过于笼统的结论?


23
尝试重构您的对象,这样您就不需要使用cast... - cjk
60
除非这样的吞吐量符合实际业务需求,否则我不会避开代码,只因为它每秒仅执行2.5亿次左右而非7.5亿次。此外,数据很可能来自比这段代码慢得多的东西(如数据库、磁盘等)。选择最方便的方式,并在要紧的地方进行优化。 - Fredrik Mörk
23
也许你应该更关注的是,你担心需要多花费3.5纳秒的时间…… - annakata
42
用这种推理方式,你应该放弃使用.NET因为它很慢。转而使用本地编译语言如C++。但C++有一些库不适用于你的特定情况,所以使用无任何库的C语言。当然,编译器会做出一些假设(尽管编译器通常比没有经验的开发人员更聪明),所以了解汇编语言并使用它来代替。总之,新的语言/功能使得编程变得更容易和更可维护,但通常不会更快。你更喜欢易于阅读的程序还是比3.5纳秒更快的程序?这取决于你的业务需求。 - Nelson Rothermel
10
根据您在此讨论串中的评论,似乎您已经坚定地决定不使用LINQ,尽管其他人已经提出了建议。我不确定您的目的是什么 - 是希望得到确认您是正确的,而不是寻找正确的解决方案吗? - AHungerArtist
显示剩余15条评论
17个回答

258

Linq是否应该被避免因为它很慢?

不应该。如果它“不够快”,则应该避免使用。 “慢”和“不够快”完全不同!

“慢”与您的客户、管理层和利益相关者无关。而“不够快”非常相关。永远不要衡量某个东西有多快;这并不能提供任何可以用来做出业务决策的信息。应该衡量它“接近客户可接受的程度”。如果已经能够接受,那么就不要再花时间和金钱来使其更快了;因为它已经足够好了。

性能优化是“昂贵的”。编写可供他人阅读和维护的代码也是“昂贵的”。这些目标通常相互矛盾,因此为了合理地花费利益相关者的资金,您必须确保只在“不够快”的情况下进行性能优化。

您找到了一种人工和不切实际的基准测试情况,在这种情况下,Linq代码比其他方式写的代码慢。我向您保证,您的客户对您不切实际基准测试的速度毫不关心。他们只关心您交付给他们的程序是否对他们而言太慢。如果他们是称职的,我向您保证,您的管理层也不会关心这个问题;他们关心的是您是否不必要地花费了多少资金来使已经足够快的东西变得更快,并在此过程中使代码更加昂贵、难以理解和维护。


29
你的意思是,既然你熟悉的魔鬼比陌生的更好,这没什么问题,但如果你喜欢根据古语来做商业决策的话就有点不妙。我认为,基于从实证测量中得出的知情意见做商业决策通常是更好的想法。你现在正在朝着正确的方向进行实证测量,这很好。但你正在将这种测量结果与错误的事物进行比较。你应该衡量的是客户满意度和股东成本,而不是每次比较的微秒数。 - Eric Lippert
10
与其“消除任何可能导致性能问题的内容”,不如先编写正确且易于维护的代码,然后在出现性能问题时逐一排查并消除(如果您的软件结构和设计良好,这应该非常容易)?我不想说得太直接,但您基本上在向全世界宣布您不知道如何完成工作。您“问题”的部分似乎是代码太“庞大”了——嗯,猜猜看,如果您有效地使用LINQ,代码就不会那么庞大。 - Aaronaught
7
@user,我保证有时 LINQ 会比您自己编写的代码执行得更快。并不是因为您不能编写比 LINQ 更快的代码,而是您没有时间这样做。您花费了所有的时间来尝试满足规格,整合经常相互矛盾的业务规则,查找和消除错误等。与此同时,微软拥有一支人员优化 Join 操作的团队。通过使用 LINQ,您将编写更具表现力且更快速地编写代码。而且它也会表现得很好。 - Anthony Pegram
13
听起来你对未来的表现有些焦虑。缓解这种焦虑的方法是建立夜间自动化性能测试政策,以实际客户指标来衡量真实产品的性能。这样每天你都可以追踪性能数字的趋势。如果你发现性能急剧下降,那么你可以检查当天的所有更改日志,并找出是什么变化导致了性能下降。不知道性能问题存在的时间越长,修复它所需的成本就越高! - Eric Lippert
18
为什么要担心呢?试着今天使用LINQ,明天你就会知道它是否导致了无法接受的性能降低。你说使用LINQ的不确定性太高;好吧,除非你花些时间使用并学习哪些方法适合你,哪些不适合你,否则你不会更加确定。如果你真正的问题是“应该避免使用LINQ,因为我的团队不知道如何有效地使用它吗?”那么这就是一个与你所问的不同的问题。对于那个问题,我的答案是“不;相反,学习如何有效地使用它!” - Eric Lippert
显示剩余8条评论

160

你为什么在使用 Cast<T>()?你没有给我们足够的代码来判断基准测试,基本上是这样。

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

LINQ 极大地增强了处理数据的代码的表达能力……只要你花时间开始理解 LINQ,编写性能良好的代码并不难。

如果有人告诉我不要使用 LINQ(特别是 LINQ to Objects)因为速度问题,我会直接笑话他们。如果他们提出具体的瓶颈,并说:“我们可以通过在这种情况下不使用LINQ来使其更快,并且这里有证据”,那就完全不同了。


4
是的,我建议去除铸件可以提高速度,但被告知即使去除铸件速度仍然很慢。我们的应用需要快速运行,所以我想,任何可能会减慢速度的东西都应该避免,但我不确定这是否是一个有效的假设。 - user455095
42
不,这绝对不是一个正确的假设。"Slow"远非一个精准的术语 - 我非常怀疑这是一个实际可行的基准。 - Jon Skeet
4
我唯一一次因性能原因删除 LINQ 的情况是在实现游戏中的人工智能时。这个特定的方法在深度内循环中被频繁地执行。我发现主要影响并不是由于 LINQ,而是由于直接索引数组和通过枚举器进行索引之间的区别(我尝试使用 foreach 进行改进,但比起切换到经典的 for 循环,效果较差)。我只做出这个改变是因为性能分析标识出代码在这里花费了 40% 的时间。 - Dan Bryant
7
如果有人因为认为LINQ(特别是LINQ to Objects)速度慢而告诉我不要使用它,我会当场嘲笑他们。实际上,出于同样的原因,我曾经在几个人面前嘲笑过。还有,当他们说“我看不到价值”的时候也是这样。 - Richard Anthony Freeman-Hein
6
我们的应用程序需要快速运行,因此我想这个想法是尽量避免任何可能会变慢的事情 - 这是我听过的最愚蠢的想法。字符串可能很慢。数组可能很慢。虚拟方法可能很慢,接口可能很慢。如果使用不当,任何东西都可能变得很慢,但这并不意味着你应该在代码的所有地方避免使用它们。 - Niki
显示剩余6条评论

86

也许我漏掉了什么,但我相当确定你的基准测试有误。

我用以下方法进行了测试:

  • Any 扩展方法("LINQ")
  • 一个简单的 foreach 循环(你的 "优化" 方法)
  • 使用 ICollection.Contains 方法
  • 使用优化的数据结构 HashSet<T>Any 扩展方法

这是测试代码:

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(),以尽可能消除噪声。

以下是结果:

      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
数据非常一致,并传达了以下信息:
- 直接使用 `Any` 扩展方法相比于直接使用 `foreach` 循环要慢约9%。 - 使用最合适的方法(`ICollection.Contains`)与未优化的数据结构(`List`)相比,速度快约50%。 - 使用优化后的数据结构(`HashSet`)在性能方面完全超越了其他方法。
我不知道你从哪里得到了243%。我的猜测是这和所有类型转换有关。如果你正在使用 `ArrayList`,那么你不仅使用了未经优化的数据结构,而且使用了基本上已经过时的数据结构。
我可以预测下一步会发生什么。"是的,我知道你可以更好地进行优化,但这只是一个例子,用来比较LINQ与非LINQ的性能。"
但是,如果你甚至不能在你的示例中深入研究,你如何可能在生产代码中做到这样的深入研究呢?
最重要的是:
如何架构和设计软件与使用特定工具及其时间相比,重要性成倍增长。
如果你遇到性能瓶颈 - LINQ与非LINQ同样可能发生 - 那就解决它们。Eric提出的自动化性能测试是一个很好的建议;这将帮助你及早地识别问题,以便你可以通过真正的解决方案来提高性能(而不是放弃一个使你的效率提高80%,但带来少于10%性能惩罚的神奇工具),并且可以将性能提升2倍、10倍、100倍甚至更多。
创建高性能应用程序不是使用正确的库。它涉及到性能分析、做出良好的设计选择和编写优秀的代码。

1
我忘了提到,这是使用 TRACE 标志 关闭 编译的,因此在此测试中不会产生任何开销。 - Aaronaught
将可枚举对象转换为哈希集合的成本如何? - Daniel B

15

LINQ是否会成为现实世界中的瓶颈(对应用程序整体或感知性能产生影响)?

你的应用程序是否会在真实世界中对10亿条以上的记录执行此操作?如果是,那么您可能需要考虑其他选择;如果不是,那就好比说"我们不能购买这款家用轿车,因为它在180英里/小时以上的速度下驾驶不好"。

如果仅仅是因为"太慢"这个理由,并不足以作为放弃LINQ的充分理由......按照这种逻辑,你应该用汇编语言/C/C++编写所有代码,而C#应该被排除在外,因为它"太慢"了。


9
这个“摇摆不定”的问题是怎么回事?你是唯一知道你的代码在做什么以及如何做的人。如果使用LINQ会使它变慢,那就不要用LINQ。如果没有性能问题,那就无所谓了,对吧? - mqp
1
@user455095:代码有性能问题吗?如果没有,那么其中的所有内容都足够快速运行,因此没有必要因为它慢而更改某些东西。如果代码确实存在性能问题,请进行分析以查看LINQ调用是否具有重大影响。如果是这样,请测试两种方式。如果LINQ更易读、更快速编写、更容易正确使用、更易于维护或类似的情况,则很可能值得一些性能损失。 - David Thornley
没有性能问题,但如果有的话,我们决定不使用LINQ而是使用标准的foreach语句,尽管它多写了几行代码,但速度更快且不必担心调整LINQ代码。 - user455095
1
我的回答是,使用非LINQ代码进行3426195426次比较也需要4944毫秒的时间,因此基于这个原因,你也不应该使用非LINQ代码... - MikeJ-UK
4
@user455095:很抱歉告诉你,你对某些遥远未来的场景可能会导致一些微不足道的性能问题的论点毫无根据。听听这些人说话;他们知道他们在说什么。Eric Lippert是构建你的C#编译器团队的成员,如果你不知道的话。Jon Skeet为谷歌工作。这两位先生和其他人在这里都试图告诉你一些东西。倾听。 - James Dunne
显示剩余4条评论

12

尽管过早的过度优化与过早的逆优化一样糟糕(在我看来),但您不应该仅基于绝对速度而排除整个技术,而不考虑使用上下文。是的,如果你需要进行一些非常繁重的数字运算并且这是一个瓶颈,LINQ可能会有问题 - 请对其进行分析。

您可以支持使用LINQ的一个论点是,虽然您可能可以通过手写代码来提高性能,但LINQ版本很可能更清晰、更易于维护 - 此外,与复杂的手动并行化相比,还有PLINQ的优势。


7
对于“过早悲观化”,我给出+1的支持。 - Lance Roberts

6
这种比较的问题在于它在抽象中是没有意义的。如果我们能通过哈希MyLinqTestClass1对象的Name属性来开始排序,那么我们就可以打败其中任何一个。如果我们能按名称排序并稍后进行二进制搜索,那么我们也可以打败另一个。事实上,我们不需要存储MyLinqTestClass1对象,只需要存储名称即可。
内存大小是一个问题吗?也许将名称存储在DAWG结构中,然后合并后缀再使用它进行此检查会更好?
设置这些数据结构的额外开销是否有意义?很难说。
还有一个不同的问题是LINQ的概念,即其名称。对于微软来说,能够说“这里有一堆很酷的新东西可以一起使用”是很好的营销手段,但是当人们在进行应该将它们分开的分析时,将它们组合在一起就不太好了。您需要调用Any,它基本上实现了.NET2.0时期常见的可枚举模式过滤(虽然.NET1.1中不常见,因为编写起来更麻烦,只有在某些情况下它的效率优势真正重要时才会使用),您还有lambda表达式和查询树都混在一起的概念。哪个是慢的?
我敢打赌这里的答案是lambda而不是使用Any,但我不会押大注(例如项目的成功),我会测试并确保。同时,lambda表达式与IQueryable一起使用可以产生特别高效的代码,如果没有使用lambda表达式,要编写等效的高效代码将非常困难。
当LINQ在效率方面表现良好却未能通过人为基准测试时,我们是否不能高效地使用它?我认为不是。
在瓶颈条件下,即使看起来合适或不合适作为优化,也应远离或转向LINQ。不要一开始就编写难以理解的代码,因为这只会使真正的优化更加困难。

这让我想起了Juval Lowy的“每个类都是WCF服务”的演讲。他对使用这种类型的场景(使用原始循环)来比较性能进行了充分的抨击,指出这不仅毫无意义,而且与实际测量结果相比通常会产生错误的结果。 - STW

4

或许linq执行速度较慢,但使用linq可以轻松地并行化我的代码。

像这样:

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

您如何并行化循环?


实际上,他们也不允许并行LINQ,因为它是LINQ,可能会遇到相同的性能问题。 - user455095
@user455095 - 在许多情况下,与非 LINQ 相比,并行 LINQ 实际上可以大大加快处理速度。当然,您可以自己实现线程并最终获得 20 行代码而不是一行。 - Nelson Rothermel
1
@user455095:快速代码的线性执行几乎肯定比略慢代码的并行执行更慢 - cjk
4
我不完全同意@ck的观点,因为有时并行执行会获得很少的收益(当某个因素迫使它无法真正并行时),但一般来说这是正确的。所以,@user455095,你不能使用更高效的方法,因为其中一部分比不那么高效的方法的一部分还要低效吗?抱歉,这不是优化,这只是迷信。 - Jon Hanna

4
既然你提到nHibernate的慢是由于LINQ的慢引起的,这里有一个有趣的观察结果。如果你正在使用LINQ to SQL(或相应的nHibernate),那么你的LINQ代码会转换成一个SQL服务器上的EXISTS查询,而你的循环代码必须首先获取所有行,然后对它们进行迭代。现在,你可以轻松地编写这样的测试,使得循环代码一次读取所有数据(单个数据库查找)进行10K次运行,但是LINQ代码实际上执行了10K个SQL查询。这可能会显示出循环版本的巨大速度优势,在现实中并不存在。实际上,单个EXISTS查询在每次扫描表和循环时都会比较快,即使在被查询的列上没有索引(如果此查询经常执行,则可能会有索引)。
我并不是说这是你的测试情况 - 我们没有足够的代码来看 - 但可能是。LINQ to Objects确实存在性能差异,但这可能根本无法转化为LINQ to SQL。你需要知道你正在测量什么以及它如何适用于你的实际需求。

+1,很好地指出了其他答案中未涉及的重要问题。OP并不理解不同的实现将具有不同的性能特征和考虑因素。 - Amy B

3
对我来说,这听起来像是您正在签订合同,雇主要么不理解LINQ,要么不了解系统的性能瓶颈。如果您正在编写带有GUI的应用程序,则使用LINQ所带来的轻微性能影响可以忽略不计。在典型的GUI / Web应用程序中,内存调用仅占所有等待时间的不到1%。您或者说您的雇主试图优化这1%。这真的有益吗?
但是,如果您正在编写一个科学或重度数学导向的应用程序,并且几乎没有磁盘或数据库访问,则我同意LINQ不是正确的选择。
顺便说一下,转换不是必需的。以下与您的第一个测试在功能上是相等的:
       for (int i = 0; i < 10000; i++)
            isInGroup = lst1.Any(item => item.Name == "9999");

当我使用包含10,000个MyLinqTestClass1对象的测试列表运行时,原始版本需要2.79秒,修改后需要3.43秒。在这些操作中节省30%的时间,这些操作可能只占用不到1%的CPU时间,这不是您时间的好用处。

1
它实际上是科学和数学重的,因此在那些情况下可能应该避免使用,但是否应该应用于所有情况呢?我想你可以说你可能需要在任何地方执行10亿次循环。 - user455095
1
@user455095 -- 你的意思是,处理10亿个项目需要额外2.5秒可能是不可接受的。如果他们关注这种程度的错误信息,那么你可能想要使用F#来解决问题;如果这是科学/数学相关的问题,那么F#可能会提供显著的优势。 - STW
@user455095 - 我同意STW的观点。如果性能如此关键,他们最好放弃使用C#来处理繁重的数学计算,而是使用GPU或至少使用纯C语言。 - Beep beep
我们实际上有很多重型科学和数学例程是用C++甚至Fortran编写的。 - user455095
@user455095 - LINQ有时可以使代码更易于阅读,有时可以使编写速度更快。如果客户愿意为您编写代码支付更多费用,那么请放弃使用LINQ。根据我的经验,短的LINQ语句是巨大的优势,而长/复杂的语句则会使代码难以理解。 - Beep beep

3

"有人告诉我,因为 .NET LINQ 太慢了,我们不应该使用它。"

根据我的经验,仅仅因为“某个人”曾经告诉过你什么技术、库或语言不好,就基于此做出决策是不明智的。

首先,这个信息来自一个可信的来源吗?如果不是,那么相信这个(也许是陌生的)人来做你的设计决策可能是一个巨大的错误。其次,这个信息今天还是否仍然适用?好吧,基于你简单而不太现实的基准测试,你得出结论:LINQ 比手动执行相同操作要慢。那么你要问自己的自然问题是:这段代码的性能是否至关重要?这段代码的性能会受到其他因素的限制吗 -- 比如数据库查询、等待 I/O 等等?

以下是我的工作方式:

  1. 确定需要解决的问题,并编写最简单的功能完整的解决方案,考虑已知的需求和限制。
  2. 确定你的实现是否真正满足了要求(速度是否足够快?资源消耗是否保持在可接受的水平?)。
  3. 如果没有通过测试,寻找优化和改进解决方案的方法,直到它通过第二个测试。这是你可能需要考虑放弃某些东西因为它太慢了。也许。但很有可能,瓶颈根本不在你预期的地方。

对我来说,这种简单的方法只有一个目的:最大化我的生产力,通过最小化我花在已经完全足够的代码上的时间。

是的,也许有一天你会发现你最初的解决方案不再适用。或者也许不会。如果确实出现了问题,那就当下解决它。我建议你避免浪费时间去解决假设(未来)的问题。


1
首先,这不是一个建议,而是一条规定。如果你可以使用foreach而不是linq语句,为什么不使用它,而不冒险执行10000次linq语句,因为foreach可能被认为更简单(不是我的话)。我想这个主题正在转变成什么是良好的编程实践,这往往更具主观性。如果你把10个程序员放在一个房间里问他们的意见,你会得到什么?10个不同的意见......还有很多争论......我开始听起来有点愤世嫉俗了。 - user455095
2
@user:“如果你可以使用foreach而不是LINQ语句,那么为什么不使用[ foreach语句]而不冒风险呢?”- 我不使用foreach语句的原因是,在权衡由于笨拙编写的命令式代码而最终引入错误的风险时,性能问题的风险微乎其微。编写一个快速但不起作用的程序很容易。修复快速但错误的程序比加速缓慢但正确的程序更难。 - Aaronaught
@user455095:我不知道是否有一种“规定性规则”说代码应该以执行速度为主要考虑因素。当然,有些特定情况下,执行速度应该优先于清晰简洁(比如游戏、国际象棋引擎、压缩代码等)。但根据我的经验,如果专注于功能而不是过分追求速度,你将大大增加交付高质量产品的机会。 @Aaronaught:非常好的表述——你已经很好地表达了我想要表达的观点。 - Martin Törnwall

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接