首页 > temp > 简明python教程 >
-
LINQ 性能分析(2)
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>
和数组内建的支持,例如 FindAll
、ForEach
、Find
、ConvertAll
和 TrueForAll
等。当然,在任何 LINQ 将会造成巨大性能影响的地方,我们均可使用传统的 foreach
或 for
循环代替。而对于那些不是很频繁执行的查询来说,你可以放心地使用 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/