目錄
- 選擇恰當的流操作方式
- 當心立即執行
- LINQ to Objects 會降低代碼的性能嗎
- 各種不同的方法
- 學到了什么
- 使用 LINQ to Objects 的代價
- 性能和簡潔:魚和熊掌不可兼得嗎
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 物件僅在當次迭代中存在,即不是集合中所有的物件都要同時存在于記憶體中,每一個迭代都包含了從檔案中讀取一行、將其分割成字串陣列、根據分割的結果創建物件等操作,一旦操作完當前物件,程式即開始讀取下一行檔案,直至處理完檔案中的所有行,
可以看到,由于我們借助了延遲執行所帶來的優勢,程式使用了更少的資源,同時記憶體的消耗也大為降低,
當心立即執行
大多數標準查詢運算子都通過迭代器實作了延遲執行,前面曾介紹過,這樣將有助于降低程式耗費的資源,不過還是有些查詢運算子破壞了這個優雅的延遲執行特性,實際上,這些查詢運算子的行為本身就需要一次性地遍歷序列中的所有元素,
通常地,那些回傳數量值,而不是序列的運算子都需要與之配合的查詢立即執行,例如 Aggregate、Average、Count、LongCount、Max、Min 和 Sum 等聚集運算子,這并沒有什么奇怪的——聚集運算的本意就是從一組集合資料中計算出一個數量值,為了計算出這個結果,運算子需要遍歷集合中的每一個元素,
除此之外,某些回傳序列,而不是數量值的運算子也需要在回傳之前完整遍歷源序列,例如 OrderBy、OrderByDescending 和 Reverse,這類運算子將改變源序列中元素的位置,為了能夠,正確地計算出序列中某個元素的位置,這些運算子需要首先對源序列進行遍歷,
讓我們繼續使用 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. 若該行不是注釋,則繼續處理,
- a. 若該行以
- (3)將該行分割成多個部分,
- (4)通過
Select運算子創建一個物件, - (5)根據
foreach中的陳述句對book物件進行操作, - (6)回到第 1 步,
通過在 Visual Studio 中一步一步地進行除錯即可清楚地看到上述每一步操作,這里我們也建議你能夠如此除錯一次,以便更清楚地了解 LINQ 查詢的執行程序,
若決定以不同的順((比如通過 orderby 子句或呼叫 Reverse 運算子)處理檔案中的每一行,上述流程則會有所改變,例如我們在查詢中添加了 Reverse 運算子,代碼如下:
...
from line in reader.Lines().Reverse()
...
此時,該查詢的執行順序變成下面這樣的,
- (1)執行
Reverse運算子,- a. 立即呼叫
Lines運算子,讀取所有行并進行反序操作,
- a. 立即呼叫
- (2)一次回圈開始,獲取
Reverse運算子回傳序列中的一行,- a. 若整個檔案已經被處理完畢,那么程序將終止,
- (3)使用
Where運算子對這一行 進行操作,- a. 若該行以
#開始,即注釋行,那么將重新回到第 2 步, - b. 若該行不是注釋,則繼續處理,
- a. 若該行以
- (4)將該行分割為多個部分,
- (5)通過
Select運算子創建一個物件, - (6)根據
foreach中的陳述句對book物件進行操作, - (7)回到第 2 步,
可以看到,Reverse 運算子將從前優美的管道流程完全破壞掉,因為它在最開始就將文本檔案中的所有行一次性地加載到了記憶體中,因此,除非確實有這樣的需要,否則不要輕易使用此類運算子,否則在處理大型資料源時將顯著降低程式的執行效率,并占用極大的記憶體,
有些轉換運算子也會破壞查詢的延遲執行特性,例如 ToArray、ToDictionary、ToList 和 ToLookup 等,雖然這些運算子回傳的也是序列,不過卻是以包含源序列中所有元素的集合形式一次性給出的,為了創建將要回傳的集合,這些運算子必須完整遍歷源序列中的每一個元素,
現在,你已經了解了某些查詢運算子的低效行為,接下來將介紹一個常見的場景,從中你會看到我們為什么要小心地使用 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 = https://www.cnblogs.com/vin-c/p/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 = https://www.cnblogs.com/vin-c/p/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>和陣列內建的支持,例如 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/
轉載請註明出處,本文鏈接:https://www.uj5u.com/net/26984.html
標籤:C#
上一篇:C# 基礎(更新中)
下一篇:C# the comparison between FileStream.Write() and StreamWriter.Write()
