.NET面試題系列[15] - LINQ:性能

.NET面試題系列目錄

當你使用LINQ to SQL時,請使用工具(好比LINQPad)查看系統生成的SQL語句,這會幫你發現問題可能發生在何處。html

提高性能的小技巧

避免遍歷整個序列

當咱們僅須要一個資料的時候,咱們能夠考慮使用First / FirstOrDefault / Take / Any等方法,它們都會在取得合乎要求的資料後退出,而不會遍歷整個序列(除非最後一個資料纔是合乎要求的哈哈)。而相似ToList / Max / Last / Sum / Contain等方法顯而易見會遍歷整個序列。面試

例如你判斷一個集合是否有成員時,請使用Any而不是Count==0。由於若是該集合有極多成員時,Count遍歷是很是消耗時間的。數據庫

避免重複枚舉同一序列

若是你在重複枚舉同一個序列,你可能會收到以下的警告:數組

通常看到這個提示,你須要一個ToList/ToDictionary/ToArray等相似的方法。重複枚舉是沒必要要且浪費時間的。另外,若是程序涉及多線程,或者你的序列含有隨機因素,你的每次枚舉的結果可能不一樣。咱們只須要枚舉同一序列一次,以後將結果儲存爲一個泛型集合便可。緩存

例如咱們的序列帶有隨機數:多線程

此時咱們會遍歷序列四次。但每次序列都會不一樣。例如若是咱們呼叫Sum方法四次,則可能會出現4個不一樣的和。咱們必須使用ToList方法強制LINQ提早執行。工具

避免毫無必要的緩存整個序列

在得到序列最後一個成員時,咱們有不少方法:性能

 

其中前兩個方法都不是最好的。當咱們調用LINQ的某些方法時,咱們緩存了整個序列,而這多是沒必要要的。咱們根本不須要將整個序列留在內存中,只須要得到最後一個成員就能夠了。單元測試

 

什麼時候使用ToList / ToArray / ToDictionary等方法

根據前面兩點,咱們能夠總結出來什麼時候使用ToList / ToArray / ToDictionary等方法:測試

  • 你肯定你須要整個序列的時候
  • 你肯定你會遍歷整個序列多於一次的時候
  • 若是序列不是很大的時候(由於ToList / ToArray / ToDictionary等方法將會在堆上分配一個序列對象)

是否返回IEnumerable<T>?

是否返回IEnumerable<T>,或者返回一個List,或者數組?注意當你返回IEnumerable<T>時,你並無開始遍歷這個序列(只有當你強制LINQ執行時,纔會執行這個返回IEnumerable<T>的方法)。固然若是數據來自遠端,你還能夠選擇IQueryable<T>,它不會把資料一股腦拉下來,而是作完全部的篩選以後,才ToList,把資料從遠端下載下來。因此在使用ORM時,若是它用到了IQueryable,請將你的查詢也寫成表達式而不是委託的形式。參考:http://www.cnblogs.com/SieAppler/p/3501475.html

另外,咱們能夠經過返回IEnumerable<T>而不是List或數組,來給予呼叫者最大的便利。(給他一個最General類型的返回)

 

SELECT N+1問題

假設你有一個父表(例如:汽車),其關聯一個子表,例如輪子(一對多)。如今你想對於全部的父表汽車,遍歷全部汽車,而後打印出來全部輪子的信息。默認的作法將是:

SELECT CarId FROM Cars;

而後對於每一個汽車:

SELECT * FROM Wheel WHERE CarId = ?

這會SELECT 2個表一共N(子表的行數)+1(父表)次,故稱爲SELECT N+1問題。

考察下面的代碼。假設album是一個表,artist是另一個表,album和artist是一對多的關係:

 

咱們知道foreach會強制LINQ執行,因而,咱們能夠想象這也是一個SELECT N+1問題的例子:先得到全部album(SELECT * FROM ALBUM),而後遍歷,對每個album的Title,檢查其是否包含關鍵字,若是符合,再去SELECT 表artist,共SELECT N+1次。咱們能夠經過LINQPAD或其餘方式檢查編譯器生成的SELECT語句數目,必定會是N+1條SQL語句。

解決方法:使用一個匿名對象做爲中間表格,預先將兩個表join到一塊兒

生成的SQL將只有一句話!

這篇文章中的第三點,就是一個典型的SELECT N+1問題。在代碼中,選擇了前100個score(一條SQL),而後對全部score進行遍歷,從表Student中得到Name的值(100條SQL)。

解決方法也在文章中給出了,就是將兩個表連到一塊兒。該文章的「聯表查詢統計」這一節,說的仍是這個問題。簡單說,仍是每次都用LINQPad工具,看看最終生成的SQL到底長啥樣。(固然還有不少其餘工具,或者最基本的就是用SQL Profiler不過比較麻煩)

LINQ to SQL的性能問題

提高從數據庫中拿數據的速度,能夠參考如下幾種方法:

  1. 在數據庫中的表中定義合適的索引和鍵
  2. 只得到你須要的列(使用ViewModel或者改進你的查詢)和行(使用IQueryable<T>)
  3. 儘量使用一條查詢而不是多條
  4. 只爲了展現數據,而不進行後續修改時,可使用AsNoTracking。它不會影響生成的SQL,但它能夠令系統少維護不少數據,從而提升性能
  5. 使用Reshaper等工具,它可能會在你寫出較差的代碼時給出提醒

咱們能夠經過不少工具來得到系統產生的SQL語句,例如LINQPAD或者SQL Profiler。在EF6中,咱們還可使用這樣的方法:

 

注意:編譯器不必定可以將你的LINQ語句翻譯爲SQL,例如字符串的IndexOf方法就不被支持。

使用LinqOptimizer提高LINQ語句的性能

LinqOptimizer能夠經過nuget得到。你能夠經過在IEnumerable<T>上調用AsQueryExpr方法來令LinqOptimizer優化你的LINQ語句。使用Run方法執行:

LINQ:替代選擇

在沒有找到性能瓶頸以前,不要過早優化。

  1. 是否存在須要長時間運行的LINQ語句?
  2. 是否在數據庫上取得數據,並運行LINQ語句?(這意味着存在一個LINQ語句到SQL的表達式轉換)
  3. 數據規模是否巨大?
  4. 是否須要重複極其屢次運行相同的LINQ語句?

LINQ VS Foreach(重複極其屢次運行相同的LINQ語句)

在什麼狀況下,LINQ反而不如Foreach表現好?二者的性能差距是怎樣的?下面的例子的序列有一千萬個成員,咱們對它們作些簡單運算。

 

結果:

 

能夠看到Foreach的表現稍好一點。LINQ的額外開銷在於lambda表達式轉換爲委託的形式,而foreach不須要。雖然這一點點額外開銷對於普通的狀況基本能夠忽略,但若是重複一千萬次,則性能可能會有較爲明顯的差別。

 

LINQ VS PLINQ(重複運行相同的LINQ語句)

顯而易見,若是咱們重複運行相同的任務,且任務之間又沒有什麼關係(不須要對結果進行彙總),此時咱們能夠想到用多線程來解決問題,重複利用系統的資源:

 

執行後只用了423毫秒。一般來講,執行的結果將等於Foreach的時間,除以系統CPU的核數量。當CPU爲雙核時,速度大概能夠提高一倍。固然,對於單核機器來講,PLINQ是沒有意義的。

當你的機器擁有多核,而且你處理相同的任務時(例如從不一樣的網站下載內容,並作相同的處理),能夠考慮使用PLINQ。不過PLINQ也須要一些額外開銷:它訪問線程池,新建線程,將任務分配到各個線程中,而後還要收集任務的結果。因此,你須要測量PLINQ是否真的能夠加快你的代碼的運行速度。

 

自定義ORM

一般,只有在以下狀況下才會考慮將本身寫的ORM投入生產使用:

  • 存在一些特定的複雜查詢,在項目中普遍出現,此時本身寫的ORM作了不少優化,表現好於EF
  • 存在一些特定的業務邏輯,例如將表達式解析爲XML等,EF沒有對應的功能
  • 你的項目對性能要求達到了很是苛刻的程度,致使EF的一些性能能夠接受的方法在你這裏變成了不能接受。例如EF使用了反射,但若是你的ORM只用於你開發的軟件,全部的狀況你均可以事先預計,那你也能夠不用反射

而大部分ORM開發出來的目標僅僅是:

  • 令查詢語法更加接近SQL
  • 加入了若干語法糖或代碼生成快捷方式,令編寫代碼速度稍微加快
  • 性能和EF相差無幾,有些甚至還不如EF
  • 沒有通過完全的測試
  • 自學使用

一般,本身開發一套ORM須要很長的時間,才能保證沒有錯誤,並用於生產環境。大部分狀況下,EF已是一個不錯的選擇。性能是雙刃劍,它可能也會毀了你的代碼,讓你的代碼難以維護。

 

LINQ性能問題:總結

  • 使用LINQPad等工具觀察生成的SQL。當你優化以後,再次在LINQPad上運行看看是否形成了可觀的性能提高。
  • 是否須要在數據庫上篩選數據,並運行LINQ語句?若是是的話,考慮返回IQueryable<T>,並考察編譯器構建的中間SQL語句。
  • 數據規模是否巨大?避免過早的ToList,返回IEnumerable/ IQueryable<T>類型的巨大規模的數據。
  • 是否須要重複極其屢次運行相同的LINQ語句?考慮使用foreach或者PLINQ來優化性能。
  • 使用LinqOptimizer來優化LINQ語句。
  • 使用Reshaper等工具,它可能會在你寫出較差的代碼時給出提醒。
  • 上MSDN,nuget查詢是否已經有了現成的方法(例如得到最後一個元素)。
  • 撰寫單元測試來保證你的優化的正確性。
相關文章
相關標籤/搜索