LINQ性能:对于C#6中的LINQ差异?

时间:2018-04-25 16:36:45

标签: c# performance linq

编辑:根据DavidG的建议,我将商品数量提高了100倍。我在发布模式中重新比较并更新了下面的结果。我还更新了代码,以防任何人只是在本地复制和粘贴并运行它。

所以我看到很多关于LINQ与for表现的帖子 - 其中大部分时间已经有几年了 - 而且我希望看到它对我自己有用。所以我写了一个小应用程序来测试它,结果是...不是我所期望的。 我的问题是:C#6中的更改和优化会使整个性能问题无关紧要吗?

(因为对于很大一部分.NET用户来说,这是一个问题,而不是一个有趣的微优化。有趣的,是的,但不是大多数人需要真正担心的事情。)

我知道从内存占用的角度来看,使用手动循环与LINQ仍然存在很好的问题,但是我的比较应用程序存在严重缺陷,或者看起来不再存在很大的速度差异。也许它在C#的后续版本中被优化了?

我的示例应用程序如下。我承认,这是一种人为的做法 - 这是一个最糟糕的情况,试图找到一个项目,这个项目将在百万项目列表中找到最后一个 - 但我根据这里的其他帖子抓住了它。

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

namespace LinqDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            new Program().Run();
        }

        public void Run()
        {
            RunPerformanceComparison();
            Console.ReadKey();
        }

        private void RunPerformanceComparison()
        {
            Func<string, bool> criteriaFunction = d => d.Equals("YES");

            var data = new string[100000000];
            for (int i = 0; i < data.Length - 1; i++)
            {
                data[i] = "NO";
            }

            data[data.Length - 1] = "YES";


            Console.WriteLine("With LINQ");
            Console.WriteLine("------------");
            DoPerformanceRunLinq(data, criteriaFunction);

            Console.WriteLine();

            Console.WriteLine("Without LINQ");
            Console.WriteLine("------------");
            DoPerformanceRunManual(data, criteriaFunction);
        }

        private void DoPerformanceRunLinq(string[] data, Func<string, bool> criteriaFunction)
        {
            Stopwatch sw = new Stopwatch();
            for (int i = 0; i < 10; i++)
            {
                sw.Start();
                var result = data.Where(criteriaFunction).Select(d => d).ToList();
                sw.Stop();
                Console.WriteLine($"Iteration {i + 1}\tElapsed: {sw.Elapsed.TotalMilliseconds.ToString("n2")} ms");
                sw.Reset();
            }
        }

        private void DoPerformanceRunManual(string[] data, Func<string, bool> criteriaFunction)
        {
            Stopwatch sw = new Stopwatch();
            for (int i = 0; i < 10; i++)
            {
                sw.Start();
                var result = GetItems(data, criteriaFunction);
                sw.Stop();
                Console.WriteLine($"Iteration {i + 1}\tElapsed: {sw.Elapsed.TotalMilliseconds.ToString("n2")} ms");
                sw.Reset();
            }
        }

        private IEnumerable<string> GetItems(string[] data, Func<string, bool> criteriaFunction)
        {
            var ret = new List<string>();

            // Not deferred; runs all at once
            for (int i = 0; i < data.Length; i++)
            {
                if (criteriaFunction(data[i]))
                {
                    ret.Add(data[i]);
                }
            }

            return ret;
        }
    }
}

这是运行它的输出(我在没有VS的命令行中运行它):

With LINQ
------------
Iteration 1     Elapsed: 602.39 ms
Iteration 2     Elapsed: 522.72 ms
Iteration 3     Elapsed: 601.15 ms
Iteration 4     Elapsed: 518.71 ms
Iteration 5     Elapsed: 511.38 ms
Iteration 6     Elapsed: 565.92 ms
Iteration 7     Elapsed: 506.51 ms
Iteration 8     Elapsed: 524.91 ms
Iteration 9     Elapsed: 540.85 ms
Iteration 10    Elapsed: 502.33 ms

Without LINQ
------------
Iteration 1     Elapsed: 496.09 ms
Iteration 2     Elapsed: 496.15 ms
Iteration 3     Elapsed: 540.53 ms
Iteration 4     Elapsed: 549.28 ms
Iteration 5     Elapsed: 404.46 ms
Iteration 6     Elapsed: 407.23 ms
Iteration 7     Elapsed: 461.39 ms
Iteration 8     Elapsed: 414.90 ms
Iteration 9     Elapsed: 405.67 ms
Iteration 10    Elapsed: 437.98 ms

超过100个百万字符串,这在for方面表现更好,但不是某些人过去声称的大量数据(我听说过10倍的差异。这甚至不是很接近。)。另外,它在内存中有1亿个字符串 - 我认为这里的优化不会通过选择手动循环与LINQ来完成。 :)事实上,我不确定这里的差异是否足以让任何人真正关心,除非你绝对,积极地需要每一个微秒的性能。我基本上称它为洗涤剂。

我在某个地方搞砸了我的应用程序,这只是一个无效的比较,还是内部改变了.NET?

1 个答案:

答案 0 :(得分:2)

现在,您正在衡量Where性能,并在最佳条件下进行测量。它针对一些常见情况进行了优化(具有特殊处理)。您正在将它与数组一起使用,而数组Where将使用您在“手动”方案中使用的几乎相同的代码来迭代它们。由于只有一个匹配,因此迭代器MoveNext方法只会被调用一次(好吧,也许两次)。长话短说 - Where和你的条件中的手动循环具有类似的性能,因为它们运行类似的代码。

如果你想观察(人为地)不良影响而不做太多改变,试试这个:

data.Select(c => c).Where(criteriaFunction).ToList();

现在传递给Where的不是数组而是“真实”IEnumerable(由Select返回),并且数组的特殊处理不适用。我使用此修改运行您的代码,然后Where执行比手动循环慢4倍。

如果这感觉不公平并且远离实际使用,你可以这样做:

class DataItem {
    public string Value { get; set; }
}

// loop version
private IEnumerable<string> GetItems(DataItem[] data, Func<string, bool> criteriaFunction) {
    var ret = new List<string>();
    // Not deferred; runs all at once
    for (int i = 0; i < data.Length; i++) {
        if (criteriaFunction(data[i].Value)) {
            ret.Add(data[i].Value);
        }
    }

    return ret;
}

// linq version
var result = data.Select(c => c.Value).Where(criteriaFunction).ToList();

这是人们可以真正使用LINQ做的事情,并且它与您的数据的循环版本相差大约4倍。当然LINQ版本可以进行优化,但重点是 - 有些情况下LINQ可能会明显变慢,特别是如果你不小心的话。

有很多这样的例子。考虑这完全无辜的LINQ Count

var result = data.Count(c => c.Value == "YES");

for类比:

private int ForCount(DataItem[] data) {
    int res = 0;
    for (int i = 0; i < data.Length; i++) {                
        if (data[i].Value == "YES") {
            res++;
        }
    }
    return res;
}

LINQ慢3倍。等等。