zoukankan      html  css  js  c++  java
  • LINQ 性能分析

    LINQ 的优势并不是提供了什么新功能,而是让我们能够用更新、更简单、更优雅的方法来实现原有的功能。不过通常来讲,这类功能所带来的就是对性能上的影响——LINQ 也不例外。本篇文章的主要目的就是让你了解 LINQ 查询对性能的影响。我们将介绍最基本的 LINQ 性能分析方法,并提供一些数据。还会给出一些常见的误区——了解这些误区之后,我们即可小心地绕开。

    一般情况下,在 .NET 框架中完成同一样工作总是会有多种不同的方法。有些时候这些不同仅仅体现在个人喜好或是代码形式的一致性上。不过在另一些情况下,个正确的选择将对整个程序起到决定性的作用。在 LINQ 中也存在这样的情况——有一些做法很适合在 LINQ 查询中使用,而有一些则应该尽量避免。

    我们还是以 LINQ to Text Files 示例程序(链接:https://www.vinanysoft.com/c-sharp/linq-to-text-files/)作为开始。其中我们可以看到选择正确的读取文本文件方法在 LINQ 查询中的重要性。

    选择恰当的流操作方式

    LINQ to Text Files 示例程序存在着一个潜在的问题,即其中使用了 ReadAllLines 方法。该方法将一次性地返回 CSV 文件中的所有内容。对于小文件来说,这并没有什么问题。不过若是文件非常大的话,那么该程序则会占用相当惊人的内存!

    问题还不止这些,这样的查询可能会影响到我们所期待的 LINQ 中延迟查询执行特性。在通常情况下,查询的执行将在需要时才开始,也就是说,一个查询仅在我们开始遍历其结果(例如使用 foreach 循环)的时候才会开始执行。不过在这里,ReadAllLines 方法将立即执行并将文件整个加载至内存中。但事实上很有可能程序并不完全需要其中的所有数据。

    LINQ to Objects 在设计时就非常推荐以延迟的方式执行查询。这种类似于流的处理方式同样也节省了资源(内存、CPU 等)。因此我们也应该尽量使用类似的方法编写程序。

    .NET 框架提供了很多种读取文本文件的方法,File.ReadAllLines 就是其中一种比较简单的。而更好的解决方案则是用 StreamReader 对象以流的方式加载文件,这样将大大节省资源,并让程序的执行更加流畅。将 StreamReader 集成到查询语句中有很多种方法,其中较为优雅的一种是创建一个自定义的查询运算符。

    Lines 查询运算符,用来从 StreamReader 对象中逐一返回文本行:

    public static class StreamReaderEnumerable
    {
        public static IEnumerable<string> Lines(this StreamReader source)
        {
            string line;
    
            if (source == null)
            throw new ArgumentNullException("source");
    
            while ((line = source.ReadLine()) != null)
            yield return line;
        }
    }
    

    Lines 查询运算符是以 StreamReader 类的扩展方法形式提供的。该运算符将依次返回由 StreamReader 所提供的源文件中的每行数据,不过在查询开始真正执行之前,它并不会加载任何的数据至内存。

    使用 Lines 查询运算符以流的方式解析 CSV 文件:

    using (StreamReader reader = new StreamReader("books.csv"))
    {
        var books = from line in reader.Lines()
                    where !line.StartsWith("#")
                    let parts = line.Split(',')
                    select new
                    {
                        Title = parts[1],
                        Publisher = parts[3],
                        Isbn = parts[0]
                    };
    }
    

    上述做法的优势在于,我们可以在操作大型文件的同时保持一个较小的内存占用。这类问题在提高查询执行效率方面至关重要。若没有仔细设计的话,查询语句经常会耗费大量的内存。

    我们来回顾一下当前版本 LINQ to Text Files 中的改变。关键在于实现了延迟求值——对象只有在需要,即开始遍历结果时才会创建,而不是在查询的一开始就一步到位。

    若我们使用 foreach 来遍历该查询的结果:

    foreach (var book in books)
    {
        Console.WriteLine(book.Isbn);
    }
    

    foreach 循环中的 book 对象仅在当次迭代中存在,即不是集合中所有的对象都要同时存在于内存中。每一个迭代都包含了从文件中读取一行、将其分割成字符串数组、根据分割的结果创建对象等操作。一旦操作完当前对象,程序即开始读取下一行文件,直至处理完文件中的所有行。

    可以看到,由于我们借助了延迟执行所带来的优势,程序使用了更少的资源,同时内存的消耗也大为降低。

    当心立即执行

    大多数标准查询运算符都通过迭代器实现了延迟执行。前面曾介绍过,这样将有助于降低程序耗费的资源。不过还是有些查询运算符破坏了这个优雅的延迟执行特性。实际上,这些查询运算符的行为本身就需要一次性地遍历序列中的所有元素。

    通常地,那些返回数量值,而不是序列的运算符都需要与之配合的查询立即执行,例如 AggregateAverageCountLongCountMaxMinSum 等聚集运算符。这并没有什么奇怪的——聚集运算的本意就是从一组集合数据中计算出一个数量值。为了计算出这个结果,运算符需要遍历集合中的每一个元素。

    除此之外,某些返回序列,而不是数量值的运算符也需要在返回之前完整遍历源序列。例如 OrderByOrderByDescendingReverse。这类运算符将改变源序列中元素的位置。为了能够,正确地计算出序列中某个元素的位置,这些运算符需要首先对源序列进行遍历。

    让我们继续使用 LINQ to Text Files 示例来详细地描述一下问题。在上一节中,我们以流的方式逐行加载源文件,而不是一次性地完全加载。如下面的代码所示:

    using (StreamReader reader = new StreamReader("books.csv"))
    {
        var books = from line in reader.Lines()
                    where !line.StartsWith("#")
                    let parts = line.Split(',')
                    select new
                    {
                        Title = parts[1],
                        Publisher = parts[3],
                        Isbn = parts[0]
                    };
    }
    
    foreach (var book in books)
    {
        Console.WriteLine(book.Isbn);
    }
    

    上述代码的执行顺序是这样的。

    • (1)一次循环开始,使用 Lines 运算符从文件中读取一行。
      • a. 若整个文件已经被处理完毕,那么过程将终止。
    • (2)使用 Where 运算符对这一行进行操作。
      • a. 若该行以 # 开始,即注释行,那么将重新回到第 1 步。
      • b. 若该行不是注释,则继续处理。
    • (3)将该行分割成多个部分。
    • (4)通过 Select 运算符创建一个对象。
    • (5)根据 foreach 中的语句对 book 对象进行操作。
    • (6)回到第 1 步。

    通过在 Visual Studio 中一步一步地进行调试即可清楚地看到上述每一步操作。这里我们也建议你能够如此调试一次,以便更清楚地了解 LINQ 查询的执行过程。

    若决定以不同的顺((比如通过 orderby 子句或调用 Reverse 运算符)处理文件中的每一行,上述流程则会有所改变。例如我们在查询中添加了 Reverse 运算符,代码如下:

    ...
    from line in reader.Lines().Reverse()
    ...
    

    此时,该查询的执行顺序变成下面这样的。

    • (1)执行 Reverse 运算符。
      • a. 立即调用 Lines 运算符,读取所有行并进行反序操作。
    • (2)一次循环开始,获取 Reverse 运算符返回序列中的一行。
      • a. 若整个文件已经被处理完毕,那么过程将终止。
    • (3)使用 Where 运算符对这一行 进行操作。
      • a. 若该行以 # 开始,即注释行,那么将重新回到第 2 步。
      • b. 若该行不是注释,则继续处理。
    • (4)将该行分割为多个部分。
    • (5)通过 Select 运算符创建一个对象。
    • (6)根据 foreach 中的语句对 book 对象进行操作。
    • (7)回到第 2 步。

    可以看到,Reverse 运算符将从前优美的管道流程完全破坏掉,因为它在最开始就将文本文件中的所有行一次性地加载到了内存中。因此,除非确实有这样的需要,否则不要轻易使用此类运算符,否则在处理大型数据源时将显著降低程序的执行效率,并占用极大的内存。

    有些转换运算符也会破坏查询的延迟执行特性,例如 ToArrayToDictionaryToListToLookup 等。虽然这些运算符返回的也是序列,不过却是以包含源序列中所有元素的集合形式一次性给出的。为了创建将要返回的集合,这些运算符必须完整遍历源序列中的每一个元素。

    现在,你已经了解了某些查询运算符的低效行为。接下来将介绍一个常见的场景,从中你会看到我们为什么要小心地使用 LINQ 以及其标准查询运算符。

    LINQ to Objects 会降低代码的性能吗

    很多时候 LINQ to Objects 并不能直接提供我们所要的结果。假如我们希望在一个给定的集合中,找到一个元素,该元素的某个指定属性的值在所有集合元素中最大。这就像是在一盒饼干中找到巧克力最多的那一块。这盒饼干就是那个集合,巧克力的多少就是要比较的那个属性。

    一开始,你可能会想到直接使用标准查询运算符中的 Max。不过 Max 运算符仅能够返回最大的值,而不是包含这个值的对象。Max 能够帮助你找到巧克力的最多数量,不过却不能告诉你具体是那一块饼干。

    在处理这个常见场景时,我们有很多种选择,包括用不同的方法使用 LINQ,或是直接使用传统的代码等。让我们先来看几种可选的、能够弥补 Max 不足的方法。

    SampleData 参考链接:https://www.vinanysoft.com/c-sharp/linq-in-action-test-data/

    各种不同的方法

    第一种方法是使用 foreach 循环:

    var books = SampleData.Books;
    Book maxBook = null;
    foreach (var book in books)
    {
        if (maxBook == null || book.PageCount > maxBook.PageCount)
        {
            maxBook = book;
        }
    }
    

    这种解决方案非常易于理解,其中保留了“目前为止页数最多的图书”的引用。这种方法只需要遍历一遍集合,其时间复杂度为 O(n),除非我们能够了解更多有关该集合的信息,否则这就是理论上最快的方法。

    第二种方法是先按照页数为集合中的图书对象排序,然后获取其中的第一个元素:

    var books = SampleData.Books;
    var sortedList = from book in books
                        orderby book.PageCount descending
                        select book;
    var maxBook = sortedList.First();
    

    在上述做法中,我们首先使用 LINQ 查询将图书集合按照页数逆序排列,随后取得排在最前面的一个元素。其缺点在于我们必须首先对整个集合进行排序,然后才能取得结果。其时间复杂度为 O(n log n)。

    第三种方法是使用子查询:

    var books = SampleData.Books;
    var maxList = from book in books
                    where book.PageCount == books.Max(b => b.PageCount)
                    select book;
    var maxBook = maxList.First();
    

    在这个方法中,我们将找到集合中页码等于最大页数的每一本书,然后取得其中的第一本。不过这种做法将在比较每个元素时都要计算一遍最大页数,让时间复杂度上升为 O(n2)。

    第四种方法是使用两个查询:

    var books = SampleData.Books;
    var maxPageCount = books.Max(book => book.PageCount);
    var maxList = from book in books
                    where book.PageCount == maxPageCount
                    select book;
    var maxBook = maxList.First();
    

    这种做法与第三种类似,不过不会每次重复地计算最大页数——一开始就先把它计算好。这样就将时间复杂度降低至 O(n),但我们仍需要遍历该集合两次。

    最后一种方法的意义在于,它能够更好地与 LINQ 集成在一起,即通过自定义的查询运算符实现。下面的代码给出了该 MaxElement 运算符的实现。

    public static TElement MaxElement<TElement, TData>(
        this IEnumerable<TElement> source,
        Func<TElement, TData> selector)
        where TData : IComparable<TData>
    {
        if (source == null)
            throw new ArgumentNullException("source");
        if (selector == null)
            throw new ArgumentNullException("selector");
    
        Boolean firstElement = true;
        TElement result = default(TElement);
        TData maxValue = default(TData);
        foreach (TElement element in source)
        {
            var candidate = selector(element);
            if (firstElement || (candidate.CompareTo(maxValue) > 0))
            {
                firstElement = false;
                maxValue = candidate;
                result = element;
            }
        }
        return result;
    }
    

    该查询运算符的使用方法非常简单:

    var maxBook = books.MaxElement(book => book.PageCount);
    

    下表给出了上述 5 种方法的运行时间,其中每种方法都执行了 20 次:

    方法                  平均时间(毫秒)            最小时间(毫秒)            最大时间(毫秒)
    foreach               4.15                        4                           5
    OrderBy + First       360.6                       316                         439
    子查询                4432.5                      4364                        4558
    两次查询              7.7                         7                           10
    自定义查询运算符      7.7                         7                           12
    

    测试环境为 Windows 10 专业版,AMD Ryzen 5 2400G with Radeon Vega Graphics 3.60 GHz CPU,32G 内存,程序均以 Release 模式编译。

    测试代码如下:

    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Linq;
    using LinqInAction.LinqBooks.Common;
    
    static class Demo
    {
        public static void Main()
        {
            BooksForPerformance();
    
            Console.WriteLine("{0,-20}{1,-20}{2,-20}{3,-20}", "方法", "平均时间(毫秒)", "最小时间(毫秒)", "最大时间(毫秒)");
    
            var time = 20;
            var result = Test(Foreach, time);
            Console.WriteLine($"{"foreach",-22}{result.avg,-28}{result.min,-28}{result.max,-28}");
    
            result = Test(OrderByAndFirst, time);
            Console.WriteLine($"{"OrderBy + First",-22}{result.avg,-28}{result.min,-28}{result.max,-28}");
    
            result = Test(Subquery, time);
            Console.WriteLine($"{"子查询",-19}{result.avg,-28}{result.min,-28}{result.max,-28}");
    
            result = Test(TwoQueries, time);
            Console.WriteLine($"{"两次查询",-18}{result.avg,-28}{result.min,-28}{result.max,-28}");
    
            result = Test(Custom, time);
            Console.WriteLine($"{"自定义查询运算符",-14}{result.avg,-28}{result.min,-28}{result.max,-28}");
    
            Console.ReadKey();
        }
    
        private static void BooksForPerformance()
        {
            var rndBooks = new Random(123);
            var rndPublishers = new Random(123);
            var publisherCount = SampleData.Publishers.Count();
    
            var result = new List<Book>();
            for (int i = 0; i < 1000000; i++)
            {
                var publisher = SampleData.Publishers.Skip(rndPublishers.Next(publisherCount)).First();
                var pageCount = rndBooks.Next(1000);
                result.Add(new Book
                {
                    Title = pageCount.ToString(),
                    PageCount = pageCount,
                    Publisher = publisher
                });
            }
    
            SampleData.Books = result.ToArray();
        }
    
        /// <summary>
        /// 第一种方法
        /// </summary>
        /// <returns></returns>
        static void Foreach()
        {
            var books = SampleData.Books;
            Book maxBook = null;
            foreach (var book in books)
            {
                if (maxBook == null || book.PageCount > maxBook.PageCount)
                {
                    maxBook = book;
                }
            }
        }
    
        /// <summary>
        /// 第二种方法
        /// </summary>
        static void OrderByAndFirst()
        {
            var books = SampleData.Books;
            var sortedList = from book in books
                             orderby book.PageCount descending
                             select book;
            var maxBook = sortedList.First();
        }
    
        /// <summary>
        /// 第三种方法
        /// </summary>
        static void Subquery()
        {
            var books = SampleData.Books;
            var maxList = from book in books
                          where book.PageCount == books.Max(b => b.PageCount)
                          select book;
            var maxBook = maxList.First();
        }
    
        /// <summary>
        /// 第四种方法
        /// </summary>
        static void TwoQueries()
        {
            var books = SampleData.Books;
            var maxPageCount = books.Max(book => book.PageCount);
            var maxList = from book in books
                          where book.PageCount == maxPageCount
                          select book;
            var maxBook = maxList.First();
        }
    
        /// <summary>
        /// 第五种方法
        /// </summary>
        static void Custom()
        {
            var books = SampleData.Books;
            var maxBook = books.MaxElement(book => book.PageCount);
        }
    
        /// <summary>
        /// 测试
        /// </summary>
        /// <param name="action"></param>
        /// <param name="time"></param>
        /// <returns></returns>
        static (double avg, long max, long min) Test(Action action, int time)
        {
            List<long> times = new List<long>();
            Stopwatch stopwatch = new Stopwatch();
    
            for (int i = 0; i < time; i++)
            {
                stopwatch.Start();
                action();
                stopwatch.Stop();
                times.Add(stopwatch.ElapsedMilliseconds);
                stopwatch.Reset();
            }
    
            return (times.Average(), times.Max(), times.Min());
        }
    
        public static TElement MaxElement<TElement, TData>(
            this IEnumerable<TElement> source,
            Func<TElement, TData> selector)
            where TData : IComparable<TData>
        {
            if (source == null)
                throw new ArgumentNullException("source");
            if (selector == null)
                throw new ArgumentNullException("selector");
    
            Boolean firstElement = true;
            TElement result = default(TElement);
            TData maxValue = default(TData);
            foreach (TElement element in source)
            {
                var candidate = selector(element);
                if (firstElement || (candidate.CompareTo(maxValue) > 0))
                {
                    firstElement = false;
                    maxValue = candidate;
                    result = element;
                }
            }
            return result;
        }
    }
    

    从上述统计数据中可以看到,不同做法之间的性能差异非常大。因此在使用 LINQ 查询之前,必须仔细斟酌!普遍来看,对集合只遍历一次的效率要比其他做法高很多。虽然与传统的、非 LINQ 的方式相比,自定义查询运算符的效率并不算最好,不过它仍遥遥领先于其他做法。因此你可以根据个人喜好选择是使用这个自定义查询运算符,还是回到传统的 foreach 解决方案中。而本人的观点是,虽然自定义查询运算符存在着一些性能开销,不过它显然是在 LINQ 上下文中的一种比较优雅的解决方案。

    学到了什么

    首先需要注意的就是 LINQ to Objects 查询的复杂度。因为我们的操作大多是耗时的循环遍历,因此更要尽可能地优化,以便节省 CPU 资源。尽量不要多次遍历同一个集合,因为这显然不是个高效的操作。换句话说,谁都不希望一次又一次地重复计算饼干上巧克力的多少。你的目标只是尽快地找到这块饼干,从而尽快开始下一步。

    我们也要考虑查询将要执行的上下文。例如,同样一段查询,在 LINQ to Objects 和 LINQ to SQL 上下文中执行的效率可能有着很大的差别。因为 LINQ to SQL 将受到 SQL 语言本身的限制,并需要按照它自己的方式解释查询语句。

    结论就是必须聪明地使用 LINQ to Objects。也要知道 LINQ to Objects 并不是所有问题的最终解决方案。在某些情况下,可能传统的方法要更好一些,例如直接使用 foreach 循环等。而在另一些情况下,虽然也能够使用 LINQ,不过可能需要通过创建自定义的查询运算符来提高执行效率。在 Python 中有这样一个哲学:Python 代码是为了简单、可读且可维护,而性能优化的部分则统统应该放在 C++ 中实现。与之对应的 LINQ 哲学则是:用 LINQ 的方法编写所有代码,而将优化的部分统统封装到自定义的查询运算符中。

    使用 LINQ to Objects 的代价

    LINQ to Objects 带来了让人惊艳的代码简洁性与可读性。而作为比较,传统的操作集合代码则显得冗长繁杂。这里将要给出一些不使用 LINQ 的理由。当然并不是真正的不使用,而是要让你知道 LINQ 在性能方面的开销。

    LINQ 所提供的最简单的查询之一就是过滤,如下面的代码所示:

    var query = from book in SampleData.Books
                where book.PageCount > 500
                select book;
    

    上述操作也可以使用传统的方法实现。下面的代码就给出了 foreach 的实现方式:

    var books = new List<Book>();
    foreach (var book in SampleData.Books)
    {
        if (book.PageCount > 500)
        {
            books.Add(book);
        }
    }
    

    下面的代码则使用了 for 循环

    var books = new List<Book>();
    for (int i = 0; i < SampleData.Books.Length; i++)
    {
        var book = SampleData.Books[i];
        if (book.PageCount > 500)
        {
            books.Add(book);
        }
    }
    

    下面的代码使用了 List<T>.FindAll 方法:

    var books = SampleData.Books.ToList().FindAll(book => book.PageCount > 500);
    

    虽然还会有其他的实现方式,不过这里的主要目的并不是将它们一一列出。为了比较每种做法的性能,我们特地随机创建了一个包含一百万个对象的集合。下表给出了在 Release 模式下运行 20 次的统计结果:

    方法                  平均时间(毫秒)            最小时间(毫秒)            最大时间(毫秒)
    foreach               18.45                       13                          55
    for                   15.2                        9                           63
    List<T>.FindAll       14.15                       11                          63
    LINQ                  27.05                       20                          77
    

    感到出人意料?还是有些失望?LINQ to Objects 似乎要比其他方法慢了很多!不过不要立即放弃 LINQ,做决定前先看看后续的测试。

    首先,这些测试结果都是基于同一个查询。若将查询略加修改,那么结果又将如何呢?例如修改 where 子句中的条件,将比较整型字段 PageCount 改为比较字符串字段 Tit1e

    var result = (from book in books
                    where book.Title.StartsWith("l")
                    select book).ToList();
    

    按照同样方式修改其他的测试代码,并再次运行 20 次。其结果将如下表所示:

    方法                  平均时间(毫秒)            最小时间(毫秒)            最大时间(毫秒)
    foreach               144.3                       136                         177
    for                   134.55                      125                         156
    List<T>.FindAll       136.45                      131                         161
    LINQ                  148.4                       136                         193
    

    测试代码如下:

    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Linq;
    using LinqInAction.LinqBooks.Common;
    
    static class Demo
    {
        public static void Main()
        {
            var books = BooksForPerformance();
    
            Console.WriteLine("{0,-20}{1,-20}{2,-20}{3,-20}", "方法", "平均时间(毫秒)", "最小时间(毫秒)", "最大时间(毫秒)");
    
            var time = 20;
            var result = Test(Foreach, books, time);
            Console.WriteLine($"{"foreach",-22}{result.avg,-28}{result.min,-28}{result.max,-28}");
    
            result = Test(For, books, time);
            Console.WriteLine($"{"for",-22}{result.avg,-28}{result.min,-28}{result.max,-28}");
    
            result = Test(FindAll, books, time);
            Console.WriteLine($"{"List<T>.FindAll",-22}{result.avg,-28}{result.min,-28}{result.max,-28}");
    
            result = Test(Linq, books, time);
            Console.WriteLine($"{"LINQ",-22}{result.avg,-28}{result.min,-28}{result.max,-28}");
    
    
    
            Console.ReadKey();
        }
    
        private static List<Book> BooksForPerformance()
        {
            var rndBooks = new Random(123);
            var rndPublishers = new Random(123);
            var publisherCount = SampleData.Publishers.Count();
    
            var result = new List<Book>();
            for (int i = 0; i < 1000000; i++)
            {
                var publisher = SampleData.Publishers.Skip(rndPublishers.Next(publisherCount)).First();
                var pageCount = rndBooks.Next(1000);
                result.Add(new Book
                {
                    Title = pageCount.ToString(),
                    PageCount = pageCount,
                    Publisher = publisher
                });
            }
    
            return result;
        }
    
        /// <summary>
        /// 第一种方法
        /// </summary>
        /// <returns></returns>
        static void Foreach(List<Book> books)
        {
            var result = new List<Book>();
            foreach (var book in books)
            {
                if (book.Title.StartsWith("l"))
                {
                    result.Add(book);
                }
            }
        }
    
        /// <summary>
        /// 第二种方法
        /// </summary>
        static void For(List<Book> books)
        {
            var result = new List<Book>();
            for (int i = 0; i < books.Count; i++)
            {
                var book = books[i];
                if (book.Title.StartsWith("l"))
                {
                    result.Add(book);
                }
            }
        }
    
        /// <summary>
        /// 第三种方法
        /// </summary>
        static void FindAll(List<Book> books)
        {
            var result = books.FindAll(book => book.Title.StartsWith("l"));
        }
    
        /// <summary>
        /// 第四种方法
        /// </summary>
        static void Linq(List<Book> books)
        {
            var result = (from book in books
                          where book.Title.StartsWith("l")
                          select book).ToList();
        }
    
    
        /// <summary>
        /// 测试
        /// </summary>
        /// <param name="action"></param>
        /// <param name="books"></param>
        /// <param name="time"></param>
        /// <returns></returns>
        static (double avg, long max, long min) Test(Action<List<Book>> action, List<Book> books, int time)
        {
            List<long> times = new List<long>();
            Stopwatch stopwatch = new Stopwatch();
    
            for (int i = 0; i < time; i++)
            {
                stopwatch.Start();
                action(books);
                stopwatch.Stop();
                times.Add(stopwatch.ElapsedMilliseconds);
                stopwatch.Reset();
            }
    
            return (times.Average(), times.Max(), times.Min());
        }
    }
    

    LINQ 的做法比前面例子中比较整型值的版本要多花费大概 5 倍的时间。这是因为字符串操作要比数值操作更加耗时。不过最有趣的则是,这一次 LINQ 的做法只比最快的做法慢一点点。两次比较的结果清楚地说明,LINQ 所带来的一些性能上额外的开销并不一定成为程序效率上的瓶颈。

    不过为什么两个测试会有如此的差异呢?当我们把 where 子句中的比较条件从整型改变为字符串之后,实际上就相应地增加了每一段代码的执行时间。这段额外的时间将应用于所有的测试代码上,不过 LINQ 所带来的性能开销则始终维持在一个相对恒定的水平上。因此可以这样认为,查询中执行的操作越少,相对而言 LINQ 所带来的性能开销则越大。

    这并没有什么值得惊讶的——凡事都有利弊,LINQ 也不会只带来好处。LINQ 需要一些额外的工作,例如创建对象和对垃圾收集器的更高依赖等。这些额外的工作让 LINQ 的执行效率极大地依赖于所要执行的查询。有些时候效率可能只会降低 5%,而有些时候则可能降低 500%。

    结论就是,不要害怕使用 LINQ,不过使用时要多加小心。对于一些简单而又频繁执行的操作,或许传统的方法更适合一些。对于简单的过滤或搜索操作,我们可以仍使用 List<T>和数组内建的支持,例如 FindAllForEachFindConvertAllTrueForAll 等。当然,在任何 LINQ 将会造成巨大性能影响的地方,我们均可使用传统的 foreachfor 循环代替。而对于那些不是很频繁执行的查询来说,你可以放心地使用 LINQ to Objects。对于那些不是对时间非常敏感的操作而言,执行时间是 60 毫秒还是 10 毫秒并不会给程序的运行带来什么显著差异。别忘了 LINQ 能够在源代码级别为你带来多么好的可读性和可维护性!

    性能和简洁:鱼和熊掌不可兼得吗

    刚刚我们看到,LINQ 似乎在兼顾代码的性能和代码的简洁清晰方面给我们出了一道难题。我们再来看一个示例程序,用来或者证明,或者推翻这个理论。这次的测试将进行分组操作。下面代码中的 LINQ 查询将图书按照出版社分组,并将分组后的结果按照出版社名称进行排序。

    var result = from book in books
                    group book by book.Publisher.Name
        into publisherBooks
                    orderby publisherBooks.Key
                    select publisherBooks;
    

    若是不使用 LINQ,那么用传统的方法也能实现同样的功能:

    var result = new SortedDictionary<string, List<Book>>();
    foreach (var book in books)
    {
        if (!result.TryGetValue(book.Publisher.Name, out var publisherBooks))
        {
            publisherBooks = new List<Book>();
            result[book.Publisher.Name] = publisherBooks;
        }
        publisherBooks.Add(book);
    }
    

    运行 20 次的结果:

    方法                  平均时间(毫秒)            最小时间(毫秒)            最大时间(毫秒)
    LINQ                  61.85                       46                          124
    Foreach               421.45                      391                         505
    

    测试代码:

    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Linq;
    using LinqInAction.LinqBooks.Common;
    
    static class Demo
    {
        public static void Main()
        {
            var books = BooksForPerformance();
    
            Console.WriteLine("{0,-20}{1,-20}{2,-20}{3,-20}", "方法", "平均时间(毫秒)", "最小时间(毫秒)", "最大时间(毫秒)");
    
            var time = 20;
            var result = Test(Linq, books, time);
            Console.WriteLine($"{"LINQ",-22}{result.avg,-28}{result.min,-28}{result.max,-28}");
    
            result = Test(Foreach, books, time);
            Console.WriteLine($"{"Foreach",-22}{result.avg,-28}{result.min,-28}{result.max,-28}");
    
            Console.ReadKey();
        }
    
        private static List<Book> BooksForPerformance()
        {
            var rndBooks = new Random(123);
            var rndPublishers = new Random(123);
            var publisherCount = SampleData.Publishers.Count();
    
            var result = new List<Book>();
            for (int i = 0; i < 1000000; i++)
            {
                var publisher = SampleData.Publishers.Skip(rndPublishers.Next(publisherCount)).First();
                var pageCount = rndBooks.Next(1000);
                result.Add(new Book
                {
                    Title = pageCount.ToString(),
                    PageCount = pageCount,
                    Publisher = publisher
                });
            }
    
            return result;
        }
    
        /// <summary>
        /// 第一种方法
        /// </summary>
        /// <returns></returns>
        static void Linq(List<Book> books)
        {
            var result = (from book in books
                          group book by book.Publisher.Name
                into publisherBooks
                          orderby publisherBooks.Key
                          select publisherBooks).ToList();
        }
    
        /// <summary>
        /// 第二种方法
        /// </summary>
        static void Foreach(List<Book> books)
        {
            var result = new SortedDictionary<string, List<Book>>();
            foreach (var book in books)
            {
                if (!result.TryGetValue(book.Publisher.Name, out var publisherBooks))
                {
                    publisherBooks = new List<Book>();
                    result[book.Publisher.Name] = publisherBooks;
                }
                publisherBooks.Add(book);
            }
        }
    
        /// <summary>
        /// 测试
        /// </summary>
        /// <param name="action"></param>
        /// <param name="books"></param>
        /// <param name="time"></param>
        /// <returns></returns>
        static (double avg, long max, long min) Test(Action<List<Book>> action, List<Book> books, int time)
        {
            List<long> times = new List<long>();
            Stopwatch stopwatch = new Stopwatch();
    
            for (int i = 0; i < time; i++)
            {
                stopwatch.Start();
                action(books);
                stopwatch.Stop();
                times.Add(stopwatch.ElapsedMilliseconds);
                stopwatch.Reset();
            }
    
            return (times.Average(), times.Max(), times.Min());
        }
    }
    

    毫无疑问,传统方法的代码更长也更复杂。虽然并不是太难以理解,不过若是对功能有更进一步的需求,那么可以想象这段代码将会越来越长,越来越复杂。而 LINQ 的版本则能够始终保持简单!

    上述两段代码中最主要的差别在于其使用了完全不同的两种理念。LINQ 版本使用的是声明式的方法,而传统的版本则通过一系列的命令实现。在 LINQ 出现之前,C# 中的代码都是命令式的,因为语言本身就是如此。命令式的代码详细地给出了执行某些操作所需要的完整步骤。而 LINQ 的声明式方法则仅仅描述了我们所期望得到的结果,对于具体的实现过程并不在意。与详细描述实现步骤不同的是,LINQ 代码则更像是对结果的直接定义。这才是二者最核心的不同之处!

    前面曾经说过,你应该已经信服于 LINQ 所带来的种种便利。那么这个新的示例程序又将要证明些什么呢?答案就是,若测试一下这两种方法的执行效率,你会看到 LINQ 版本要快于传统的代码!

    当然,你可能会对产生这样的结果存有疑惑,不过我们将把调查研究的工作留给你自己。这里我们要说的是:若你希望在传统代码中得到与 LINQ 同样的执行效率,可能需要继续编写更加复杂的代码。

    从内存占用以及插入时间等角度考虑,SortedDictionary 是一个比较低效的数据结构。此外,我们还在每一次循环中使用了 TryGetValue。而 LINQ 运算符则能够更有效地处理
    这类场景。当然,这个非 LINQ 版本的代码也存在着性能提升的空间,不过同时也会带来更高的复杂性。

    原文链接:https://www.vinanysoft.com/c-sharp/linq-performance-analysis/

  • 相关阅读:
    sql基本语法:
    mysqldump: Couldn't execute 'SET OPTION SQL_QUOTE_SHOW_CREATE=1': You have an error in your SQL syntax; check the manual t
    truncate和 delete的区别:
    主流存储引擎详解:Innodb,Tokudb、Memory、MYISAM、Federated
    ant-design-vue表单生成组件form-create快速上手
    vue自定义表单生成器,可根据json参数动态生成表单
    Vue数据驱动表单渲染,轻松搞定form表单
    PHP表单生成器,快速生成现代化的form表单,快速上手
    form-create 组件生成规则说明
    form-create教程:自定义布局,实现一行多个组件
  • 原文地址:https://www.cnblogs.com/vin-c/p/13024039.html
Copyright © 2011-2022 走看看