一个LINQ语句比一个'foreach'循环更快吗?

160

我正在编写一个网格渲染管理器,认为将使用相同着色器的所有网格分组并在该着色器通道中一起渲染是个好主意。

目前我正在使用 foreach 循环,但想知道是否利用 LINQ 可以提高性能?


2
可能是"嵌套foreach" vs "lambda/linq查询"性能(LINQ-to-Objects)的重复问题。 - Daniel Earwicker
1
请考虑将@MarcGravell的答案设置为被接受的答案,因为在某些情况下,例如linq to sql,linq比for/foreach更快。 - crthompson
8个回答

287

为什么LINQ会更快呢?它内部也使用循环。

大多数情况下,因为引入了开销,LINQ会稍微慢一些。如果您非常关注性能,请不要使用LINQ。如果您想要更短、更易读、更易维护的代码,请使用LINQ。


9
那么你的经验是LINQ更快,但使代码难以阅读和维护?请解释。 - codymanix
128
我认为你的理解是相反的。他说LINQ更慢,因为存在额外开销。同时他也表示,LINQ更易于阅读和维护。 - Joseph McIntyre
9
抱歉,在此期间,我们进行了很多比较 LINQ 和 for 或 foreach 的性能测试,大多数情况下 LINQ 更快。 - Offler
58
在我看来,使用 foreach 循环比 LINQ 方法更易读。不过我还是会用 LINQ,因为它很酷 :) - LuckyLikey
5
但在某些情况下,LINQ 可能确实提高可读性,所以请忘记我的无意义评论 <3。 - LuckyLikey
显示剩余4条评论

70

LINQ-to-Objects通常会增加一些轻微的开销(多个迭代器等)。它仍然需要执行循环,而且需要委托调用,并且通常必须做一些额外的解引用才能访问捕获的变量等。在大多数代码中,这将几乎无法检测到,并且比简单易懂的代码所能承受的要多。

对于其他LINQ提供程序(如LINQ-to-SQL),由于查询可以在服务器上进行过滤,因此它应该比简单的foreach好得多,但最可能你不会对整个"select * from foo"进行操作,因此这并不一定是一个公平的比较。

关于PLINQ; 并行性可以减少经过的时间,但总CPU时间通常会稍微增加一些,因为需要管理线程等开销。


在另一个答案中,您提到不要在内存集合(例如List<Foo>)上使用LINQ,而是应该在这些集合上使用foreach块。在这些情况下建议使用foreach确实是有道理的。我的疑虑是:我是否只有在检测到性能问题时才应该用foreach替换LINQ查询?今后,我将优先考虑使用foreach - IAbstract

25

LINQ现在较慢,但可能在某个时候会变得更快。 LINQ的好处在于您不必关心它如何工作。如果想出了一种非常快的新方法,Microsoft的人员可以实现它,而无需通知您,您的代码将更快。

更重要的是,LINQ更易于阅读。这应该足够理由。


3
我喜欢这句话“微软可以实现它”,不升级框架的情况下,这是否可能呢? - Shrivallabh
3
LINQ永远不会比本地实现更快,因为归根结底,它会转换为本地实现。没有特殊的LINQ CPU指令和LINQ寄存器可用于翻译更快的LINQ机器代码 - 如果有的话,它们也将被非LINQ代码使用。 - mg30rg
不是绝对的,某些链接操作可能在某个时候变成多线程甚至利用GPU。 - John Stock

20

12

如果您在多核心环境下使用并行LINQ,可能会获得更好的性能提升。请参见 Parallel LINQ (PLINQ)(MSDN)。


9

我对这个问题很感兴趣,所以我刚才做了一个测试。在一台运行Microsoft Windows 7 Ultimate操作系统的Intel(R) Core(TM) i3-2328M CPU @ 2.20GHz,2200 Mhz,2 Core(s),并且有8GB RAM的计算机上,使用.NET Framework 4.5.2。

看起来LINQ可能比for each循环更快. 这里是我的测试结果:

Exists = True
Time   = 174
Exists = True
Time   = 149

如果你们中的一些人能够将这段代码复制并粘贴到控制台应用程序中进行测试,那将非常有趣。 在使用对象(Employee)进行测试之前,我尝试了相同的整数测试。LINQ在那里也更快。

public class Program
{
    public class Employee
    {
        public int id;
        public string name;
        public string lastname;
        public DateTime dateOfBirth;

        public Employee(int id,string name,string lastname,DateTime dateOfBirth)
        {
            this.id = id;
            this.name = name;
            this.lastname = lastname;
            this.dateOfBirth = dateOfBirth;

        }
    }

    public static void Main() => StartObjTest();

    #region object test

    public static void StartObjTest()
    {
        List<Employee> items = new List<Employee>();

        for (int i = 0; i < 10000000; i++)
        {
            items.Add(new Employee(i,"name" + i,"lastname" + i,DateTime.Today));
        }

        Test3(items, items.Count-100);
        Test4(items, items.Count - 100);

        Console.Read();
    }


    public static void Test3(List<Employee> items, int idToCheck)
    {

        Stopwatch s = new Stopwatch();
        s.Start();

        bool exists = false;
        foreach (var item in items)
        {
            if (item.id == idToCheck)
            {
                exists = true;
                break;
            }
        }

        Console.WriteLine("Exists=" + exists);
        Console.WriteLine("Time=" + s.ElapsedMilliseconds);

    }

    public static void Test4(List<Employee> items, int idToCheck)
    {

        Stopwatch s = new Stopwatch();
        s.Start();

        bool exists = items.Exists(e => e.id == idToCheck);

        Console.WriteLine("Exists=" + exists);
        Console.WriteLine("Time=" + s.ElapsedMilliseconds);

    }

    #endregion


    #region int test
    public static void StartIntTest()
    {
        List<int> items = new List<int>();

        for (int i = 0; i < 10000000; i++)
        {
            items.Add(i);
        }

        Test1(items, -100);
        Test2(items, -100);

        Console.Read();
    }

    public static void Test1(List<int> items,int itemToCheck)
    {

        Stopwatch s = new Stopwatch();
        s.Start();

        bool exists = false;
        foreach (var item in items)
        {
            if (item == itemToCheck)
            {
                exists = true;
                break;
            }
        }

        Console.WriteLine("Exists=" + exists);
        Console.WriteLine("Time=" + s.ElapsedMilliseconds);

    }

    public static void Test2(List<int> items, int itemToCheck)
    {

        Stopwatch s = new Stopwatch();
        s.Start();

        bool exists = items.Contains(itemToCheck);

        Console.WriteLine("Exists=" + exists);
        Console.WriteLine("Time=" + s.ElapsedMilliseconds);

    }

    #endregion

}

这是我得到的: 存在=True 时间=274 存在=True 时间=314 - PmanAce
2
你是否考虑先使用LINQ,然后再使用foreach,这样可能会有所不同。 - Muhammad Mamoor Khan
4
有趣。我得到了“存在=True 时间=184 存在=True 时间=135”的结果。这是在一台Apache Gaming 笔记本电脑上(Win 10,C# 7.3)编译和以调试模式运行的。如果我反转测试,我得到“存在=True 时间=158 存在=True 时间=194”的结果。看来Linq更加优化了。 - James Wilkins
1
这篇文章中关于对象test存在误解。虽然List.Exists和.Contains似乎比foreach表现更好,但需要注意的是.Exists不是一个Linq to Entities方法,只能用于列表,它的Linq等效方法.Any()的性能肯定比foreach慢。 - AbdulG
2
所有这些不准确的答案和评论。不,LINQ迭代器永远不会比foreach更快。此外,List.Exists不是LINQ方法 - l33t

4
这实际上是一个相当复杂的问题。Linq使某些事情变得非常容易做到,如果你自己实现它们,你可能会遇到困难(例如linq .Except())。这尤其适用于PLinq,特别是由PLinq实现的并行聚合。
一般来说,对于相同的代码,linq会更慢,因为委托调用的开销。
但是,如果您正在处理大量的数据数组,并对元素应用相对简单的计算,如果:
1.您使用数组存储数据。 2.您使用for循环访问每个元素(而不是foreach或linq)。
那么你将获得巨大的性能提升。
注:在基准测试时,请所有人记住-如果您为两个连续的测试使用相同的数组/列表,则CPU高速缓存将使第二个测试变快。*

0

.NET core 7即将发布,其中包括一些重要的更新,针对LINQ中的.Min、.Max、.Average和.Sum进行了性能优化。 参考链接: https://devblogs.microsoft.com/dotnet/performance_improvements_in_net_7/#linq

以下是该帖子中提供的基准测试结果。 Benchmark of .NET Core 6 vs 7 LINQ methods

如果与ForEach循环进行比较,则可以发现在.NET 6中,ForEach循环更快,在.NET 7中,LINQ方法更快: enter image description here

这是使用BenchmarkDotNet进行基准测试的代码。

using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Running;

public class Program
{
    public static void Main()
    {
        BenchmarkRunner.Run<ForEachVsLinq>();
    }
}

[SimpleJob(RuntimeMoniker.Net60)]
[SimpleJob(RuntimeMoniker.Net70)]
[MemoryDiagnoser(false)]
public class ForEachVsLinq
{
    private int[] _intArray;

    [GlobalSetup]
    public void Setup()
    {
        var random = new Random();
        var randomItems = Enumerable.Range(0, 500).Select(_ => random.Next(999));
        this._intArray = randomItems.ToArray();
    }

    [Benchmark]
    public void ForEachMin()
    {
        var min = int.MaxValue;
        foreach (var i in this._intArray)
        {
            if ( i < min)
                min = i;
        }
        Console.WriteLine(min);
    }

    [Benchmark]
    public void Min()
    {
        var min = this._intArray.Min();
        Console.WriteLine(min);
    }

    [Benchmark]
    public void ForEachMax()
    {
        var max = 0;
        foreach (var i in this._intArray)
        {
            if (i > max)
                max = i;
        }
        Console.WriteLine(max);
    }

    [Benchmark]
    public void Max()
    {
        var max = this._intArray.Max();
        Console.WriteLine(max);
    }


    [Benchmark]
    public void ForEachSum()
    {
        var sum = 0;
        foreach (var i in this._intArray)
        {
            sum += i;
        }
        Console.WriteLine(sum);
    }

    [Benchmark]
    public void Sum()
    {
        var sum = this._intArray.Sum();
        Console.WriteLine(sum);
    }
}

在.NET Core 6及更早版本中,所提到的方法比使用自己编写的foreach循环并查找数组中的最小值、最大值、平均值或汇总对象要慢。
但是在.NET Core 7中,性能提高使得这些内置的LINQ方法实际上更快。 Nick Chapsas在YouTube上的基准测试视频中展示了这一点。
因此,如果您想计算总和、最小值、最大值或平均值,应该从.NET Core 7开始使用LINQ方法,而不是foreach循环(至少从性能角度来看)。

5
从性能角度考虑,在.NET Core 7及以后,建议使用LINQ而不是foreach。您的信息比较的是LINQ 6.0和LINQ 7.0,而问题是要比较LINQ和foreach。您的比较与所问问题不相关,并且您的结论是不合乎逻辑的。 - Theodor Zoulias
@TheodorZoulias 我详细阐述了我的推理,希望现在这个问题有意义了,因为它与手头的问题相关。OP是关于分组的,所以这是另一种方法,所以我的帖子不是直接回答那个问题,但这是一个微妙之处,是否foreach循环总是比LINQ更快,正如一些答案所述。 - Daniël Tulp
那么你的答案可能在这里更相关:Min()和Max()或单个oldschool foreach? 但是,如果它包含不相关的基准测试,它仍然不是一个好答案。如果您可以展示比较LINQ方法与手动foreach实现的基准测试,那就可以了。 - Theodor Zoulias
2
根据您的要求,我添加了一个基准测试;数据并不是非常大,因此差异并不是很大,但对我来说结果是清晰的。 - Daniël Tulp

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