【问题标题】:For vs. Linq - Performance vs. FutureFor vs. Linq - 性能 vs. 未来
【发布时间】:2013-01-31 09:10:20
【问题描述】:

非常简短的问题。我有一个随机排序的大字符串数组(100K+ 条目),我想在其中找到所需字符串的第一次出现。我有两个解决方案。

通过阅读我的猜测是,“for 循环”目前会提供稍微更好的性能(但这个边距总是会改变),但我也发现 linq 版本更具可读性。总的来说,哪种方法通常被认为是当前的最佳编码实践,为什么?

string matchString = "dsf897sdf78";
int matchIndex = -1;
for(int i=0; i<array.length; i++)
{
    if(array[i]==matchString)
    {
        matchIndex = i;
        break;
    }
}

int matchIndex = array.Select((r, i) => new { value = r, index = i })
                         .Where(t => t.value == matchString)
                         .Select(s => s.index).First();

【问题讨论】:

  • 在这种情况下我什至不会使用 LINQ,因为你真的必须努力寻找索引 - 我会使用 Array.IndexOf :)
  • 我在大型数据表(100k+ 条记录,约 40 列)上使用 LINQ,没有任何性能问题。
  • @hometoast 我不使用 Linq2Sql。我使用 LINQ 搜索、分组和过滤数据表。而且 DataTable 并不总是 SQL 操作的结果。
  • 然后撤回评论。

标签: c# performance linq


【解决方案1】:

最佳实践取决于您的需要:

  1. 开发速度和可维护性:LINQ
  2. 性能(根据分析工具):手动代码

LINQ 确实通过所有间接方式减慢了速度。不用担心,因为 99% 的代码不会影响最终用户的性能。

我从 C++ 开始,真正学会了如何优化一段代码。 LINQ 不适合充分利用 CPU。因此,如果您将 LINQ 查询衡量为一个问题,那就放弃它。但只有那时。

对于您的代码示例,我估计会减速 3 倍。通过 lambda 进行分配(以及随后的 GC!)和间接访问真的很痛苦。

【讨论】:

  • 同意。 Linq 的性能成本很小,但在许多情况下可以忽略不计。事实上,我记得 StackOverflow 背后的大部分代码都使用了 Linq
  • +1 并且想补充一点,只有 20% 的代码运行 80% 的时间,所以如果出现性能问题,只需要优化瓶颈
  • 通过 lambda 的间接传递真的很痛我不同意。一旦对表达式求值,JIT 就会找到一种方法来避免虚函数调用开销。
  • @ozgur JVM HotSpot 编译器通常可以做到这一点。 .NET JIT从不去虚拟化调用,即使调用目标类型是静态已知的,通常也不会。在任何情况下,委托调用都不会被虚拟化。
【解决方案2】:

稍微更好的性能?循环将显着提高性能!

考虑下面的代码。在我的 RELEASE(非调试)构建系统上,它给出:

Found via loop at index 999999 in 00:00:00.2782047
Found via linq at index 999999 in 00:00:02.5864703
Loop was 9.29700432810805 times faster than linq.

特意设置了代码,以便要找到的项目就在最后。如果一开始是对的,事情就会完全不同。

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;

namespace Demo
{
    public static class Program
    {
        private static void Main(string[] args)
        {
            string[] a = new string[1000000];

            for (int i = 0; i < a.Length; ++i)
            {
                a[i] = "Won't be found";
            }

            string matchString = "Will be found";

            a[a.Length - 1] = "Will be found";

            const int COUNT = 100;

            var sw = Stopwatch.StartNew();
            int matchIndex = -1;

            for (int outer = 0; outer < COUNT; ++outer)
            {
                for (int i = 0; i < a.Length; i++)
                {
                    if (a[i] == matchString)
                    {
                        matchIndex = i;
                        break;
                    }
                }
            }

            sw.Stop();
            Console.WriteLine("Found via loop at index " + matchIndex + " in " + sw.Elapsed);
            double loopTime = sw.Elapsed.TotalSeconds;

            sw.Restart();

            for (int outer = 0; outer < COUNT; ++outer)
            {
                matchIndex = a.Select((r, i) => new { value = r, index = i })
                             .Where(t => t.value == matchString)
                             .Select(s => s.index).First();
            }

            sw.Stop();
            Console.WriteLine("Found via linq at index " + matchIndex + " in " + sw.Elapsed);
            double linqTime = sw.Elapsed.TotalSeconds;

            Console.WriteLine("Loop was {0} times faster than linq.", linqTime/loopTime);
        }
    }
}

【讨论】:

  • 问题是降低 linq 查询速度的新运算符。如果可以将数组转换为列表,则可以将 linq 与 FindIndex 结合使用,这一次 for 循环仅快 1.5 倍左右。 'matchIndex = a.ToList().FindIndex(x => x.Equals(matchString));'
  • 将您的查询更改为更接近常规循环的内容,大大减少了差异:string tst = a.First(s =&gt; matchIndex++ !=-2 &amp;&amp; s == matchString);
  • @jmoreno 好吧,这不足为奇......虽然在我的 PC 上发布版本,但循环仍然快了 3 倍以上。
  • 老兄!您的 linq 查询错误!正确的是下面的,这个慢了不到 10%。 matchIndex = a.Where(t =&gt; t == matchString).Select((r, i) =&gt; i).First();
  • 我使用了您的示例并进行了一些更改,将字符串更改为 List 并使用 a.IndexOf(a.Find(o => o == matchString));有所作为。输出变为“通过 linq 在 00:00:00.0221552 的索引 999999 处找到”
【解决方案3】:

根据声明性范式,LINQ 表达了计算的逻辑,而不描述其控制流。该查询是面向目标的、自我描述的,因此易于分析和理解。也很简洁。此外,使用 LINQ,高度依赖于数据结构的抽象。这涉及到较高的可维护性和可重用性。

迭代方法解决了命令式范式。它提供了细粒度的控制,从而轻松获得更高的性能。代码也更易于调试。有时结构良好的迭代比查询更具可读性。

【讨论】:

    【解决方案4】:

    性能和可维护性之间总是存在两难选择。通常(如果没有关于性能的特定要求)可维护性应该是赢家。只有当您遇到性能问题时,您才应该分析应用程序,找到问题根源并提高其性能(通过同时降低可维护性,是的,这就是我们生活的世界)。

    关于您的样品。 Linq 在这里不是很好的解决方案,因为它不会将匹配可维护性添加到您的代码中。实际上,对我来说,投影、过滤和再次投影看起来比简单的循环更糟糕。你需要的是简单的 Array.IndexOf,它比循环更易于维护,并且性能几乎相同:

    Array.IndexOf(array, matchString)
    

    【讨论】:

      【解决方案5】:

      嗯,你自己回答了你的问题。

      如果您想要获得最佳性能,请使用For 循环,如果您想要可读性,请使用Linq

      也许还要记住使用 Parallel.Foreach() 的可能性,这将受益于内联 lambda 表达式(因此,更接近 Linq),并且比“手动”进行并行化更具可读性。

      【讨论】:

      • 我一直想知道为什么 LINQ 和 lambda 表达式自动被认为更具可读性。有时一个简单的 foreach 或 for 比 LINQ IMO 更具可读性
      • @LeeDale 当然。我想补充一下,我的答案是关于 Linq 的流利风格布局,就像在问题中一样,而不是声明式风格。
      【解决方案6】:

      我认为这两种方法都不是最佳实践,有些人喜欢看 LINQ,有些人不喜欢。

      如果性能是一个问题,我会为您的场景分析这两种代码,如果差异可以忽略不计,那么选择您觉得更符合的那个,毕竟很可能是您维护代码。

      您是否还考虑过使用 PLINQ 或使循环并行运行?

      【讨论】:

        【解决方案7】:

        最好的选择是使用 Array 类的 IndexOf 方法。由于它专门用于数组,因此它比 Linq 和 For Loop 快得多。 改进 Matt Watsons 的答案。

        using System;
        using System.Diagnostics;
        using System.Linq;
        
        
        namespace PerformanceConsoleApp
        {
            public class LinqVsFor
            {
        
                private static void Main(string[] args)
                {
                    string[] a = new string[1000000];
        
                    for (int i = 0; i < a.Length; ++i)
                    {
                        a[i] = "Won't be found";
                    }
        
                    string matchString = "Will be found";
        
                    a[a.Length - 1] = "Will be found";
        
                    const int COUNT = 100;
        
                    var sw = Stopwatch.StartNew();
        
                    Loop(a, matchString, COUNT, sw);
        
                    First(a, matchString, COUNT, sw);
        
        
                    Where(a, matchString, COUNT, sw);
        
                    IndexOf(a, sw, matchString, COUNT);
        
                    Console.ReadLine();
                }
        
                private static void Loop(string[] a, string matchString, int COUNT, Stopwatch sw)
                {
                    int matchIndex = -1;
                    for (int outer = 0; outer < COUNT; ++outer)
                    {
                        for (int i = 0; i < a.Length; i++)
                        {
                            if (a[i] == matchString)
                            {
                                matchIndex = i;
                                break;
                            }
                        }
                    }
        
                    sw.Stop();
                    Console.WriteLine("Found via loop at index " + matchIndex + " in " + sw.Elapsed);
        
                }
        
                private static void IndexOf(string[] a, Stopwatch sw, string matchString, int COUNT)
                {
                    int matchIndex = -1;
                    sw.Restart();
                    for (int outer = 0; outer < COUNT; ++outer)
                    {
                        matchIndex = Array.IndexOf(a, matchString);
                    }
                    sw.Stop();
                    Console.WriteLine("Found via IndexOf at index " + matchIndex + " in " + sw.Elapsed);
        
                }
        
                private static void First(string[] a, string matchString, int COUNT, Stopwatch sw)
                {
                    sw.Restart();
                    string str = "";
                    for (int outer = 0; outer < COUNT; ++outer)
                    {
                        str = a.First(t => t == matchString);
        
                    }
                    sw.Stop();
                    Console.WriteLine("Found via linq First at index " + Array.IndexOf(a, str) + " in " + sw.Elapsed);
        
                }
        
                private static void Where(string[] a, string matchString, int COUNT, Stopwatch sw)
                {
                    sw.Restart();
                    string str = "";
                    for (int outer = 0; outer < COUNT; ++outer)
                    {
                        str = a.Where(t => t == matchString).First();
        
                    }
                    sw.Stop();
                    Console.WriteLine("Found via linq Where at index " + Array.IndexOf(a, str) + " in " + sw.Elapsed);
        
                }
        
            }
        
        }
        

        输出:

        Found via loop at index 999999 in 00:00:01.1528531
        Found via linq First at index 999999 in 00:00:02.0876573
        Found via linq Where at index 999999 in 00:00:01.3313111
        Found via IndexOf at index 999999 in 00:00:00.7244812
        

        【讨论】:

          【解决方案8】:

          有点无法回答,实际上只是对https://stackoverflow.com/a/14894589 的扩展,但我一直在研究与 API 兼容的 Linq-to-Objects 替代品。它仍然不提供手动编码循环的性能,但对于许多(大多数?)linq 场景来说它更快。它确实会产生更多的垃圾,并且前期成本会稍高一些。

          代码可用https://github.com/manofstick/Cistern.Linq

          可以使用 nuget 包https://www.nuget.org/packages/Cistern.Linq/(我不能声称这是经过实战的,使用风险自负)

          从 Matthew Watson 的答案 (https://stackoverflow.com/a/14894589) 中获取代码并稍作调整,我们将时间缩短到“仅”比手动编码循环差约 3.5 倍。在我的机器上大约需要原始 System.Linq 版本的 1/3 时间。

          要替换的两个更改:

          using System.Linq;
          
          ...
          
          matchIndex = a.Select((r, i) => new { value = r, index = i })
                       .Where(t => t.value == matchString)
                       .Select(s => s.index).First();
          

          以下内容:

          // a complete replacement for System.Linq
          using Cistern.Linq;
          
          ...
          
          // use a value tuple rather than anonymous type
          matchIndex = a.Select((r, i) => (value: r, index: i))
                       .Where(t => t.value == matchString)
                       .Select(s => s.index).First();
          

          所以图书馆本身是一项正在进行的工作。它未能通过 corefx 的 System.Linq 测试套件中的几个边缘案例。它还需要转换一些函数(它们目前具有 corefx System.Linq 实现,从 API 角度来看,如果不是从性能角度来看,它是兼容的)。但是,如果有人愿意提供帮助、评论等,我们将不胜感激......

          【讨论】:

            【解决方案9】:

            只是一个有趣的观察。 LINQ Lambda 查询肯定会增加 LINQ Where 查询或 For 循环的惩罚。在下面的代码中,它使用 1000001 个多参数对象填充一个列表,然后使用 LINQ Lamba、LINQ Where 查询和 For 循环搜索在此测试中将始终是最后一个的特定项目。每个测试迭代 100 次,然后平均得到结果。

            LINQ Lambda 查询平均时间:0.3382 秒

            LINQ Where 查询平均时间:0.238 秒

            For 循环平均时间:0.2266 秒

            我一遍又一遍地运行这个测试,甚至增加了迭代次数,从统计学上讲,分布几乎相同。当然,我们所说的基本上是一百万个项目搜索的 1/10 秒。所以在现实世界中,除非有那么密集的事情,否则你不确定你是否会注意到。但是,如果您执行 LINQ Lambda 与 LINQ Where 查询确实会在性能上有所不同。 LINQ Where 与 For 循环几乎相同。

            private void RunTest()
            {
                try
                {
                    List<TestObject> mylist = new List<TestObject>();
            
                    for (int i = 0; i <= 1000000; i++)
                    {
                        TestObject testO = new TestObject(string.Format("Item{0}", i), 1, Guid.NewGuid().ToString());
                        mylist.Add(testO);
                    }
            
            
                    mylist.Add(new TestObject("test", "29863", Guid.NewGuid().ToString()));
            
                    string searchtext = "test";
            
                    int iterations = 100;
            
                    // Linq Lambda Test
                    List<int> list1 = new List<int>();
                    for (int i = 1; i <= iterations; i++)
                    {
                        DateTime starttime = DateTime.Now;
                        TestObject t = mylist.FirstOrDefault(q => q.Name == searchtext);
                        int diff = (DateTime.Now - starttime).Milliseconds;
                        list1.Add(diff);
                    }
            
                    // Linq Where Test
                    List<int> list2 = new List<int>();
                    for (int i = 1; i <= iterations; i++)
                    {
                        DateTime starttime = DateTime.Now;
                        TestObject t = (from testO in mylist
                                        where testO.Name == searchtext
                                        select testO).FirstOrDefault();
                        int diff = (DateTime.Now - starttime).Milliseconds;
                        list2.Add(diff);
                    }
            
                    // For Loop Test
                    List<int> list3 = new List<int>();
                    for (int i = 1; i <= iterations; i++)
                    {
                        DateTime starttime = DateTime.Now;
                        foreach (TestObject testO in mylist)
                        {
                            if (testO.Name == searchtext)
                            {
                                TestObject t = testO;
                                break;
                            }
                        }
                        int diff = (DateTime.Now - starttime).Milliseconds;
                        list3.Add(diff);
                    }
            
                    float diff1 = list1.Average();
                    Debug.WriteLine(string.Format("LINQ Lambda Query Average Time: {0} seconds", diff1 / (double)100));
            
                    float diff2 = list2.Average();
                    Debug.WriteLine(string.Format("LINQ Where Query Average Time: {0} seconds", diff2 / (double)100));
            
                    float diff3 = list3.Average();
                    Debug.WriteLine(string.Format("For Loop Average Time: {0} seconds", diff3 / (double)100));
                }
                catch (Exception ex)
                {
                    Debug.WriteLine(ex.ToString());
                }
            }
            
            private class TestObject
            {
                public TestObject(string _name, string _value, string _guid)
                {
                    Name = _name;
                    Value = _value;
                    GUID = _guid;
                }
                public string Name;
                public string Value;
                public string GUID;
            }
            

            【讨论】:

            • 您在哪台机器上运行测试?运行它的机器的速度是否重要?例如,如果我们在 Xamarin.Android 中使用 linq,那么我们关心的是在移动设备中运行应用程序的速度吗?
            • 机器的速度应该无关紧要,因为它会比较同一台机器上不同操作的速度。
            猜你喜欢
            • 2023-03-17
            • 2014-05-16
            • 1970-01-01
            • 2023-03-18
            • 2017-03-31
            • 1970-01-01
            • 2017-07-12
            • 2013-09-09
            相关资源
            最近更新 更多