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


.NET面試題系列目錄

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

提升性能的小技巧

避免遍歷整個序列

當我們僅需要一個資料的時候,我們可以考慮使用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查詢是否已經有了現成的方法(例如獲得最后一個元素)。
  • 撰寫單元測試來保證你的優化的正確性。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM