Linq to Objects性能優(yōu)化知多少

最近工作上遇到了一個(gè)性能優(yōu)化的問(wèn)題煤蚌,程序批量提交2000行數(shù)據(jù)饺窿,導(dǎo)致將近10分鐘才執(zhí)行完畢。
拿到這樣的性能問(wèn)題签赃,首先是進(jìn)行Sql Server Profiler監(jiān)控Sql執(zhí)行情況溃斋。分析可能存在耗時(shí)的SQL語(yǔ)句界拦。
通過(guò)監(jiān)控發(fā)現(xiàn)耗時(shí)最久的Sql語(yǔ)句批量查詢6萬(wàn)數(shù)據(jù),耗時(shí)10s梗劫,分析執(zhí)行計(jì)劃享甸,也沒(méi)有優(yōu)化的點(diǎn)。就轉(zhuǎn)向Visual Studio Profiler看看代碼中是否有耗時(shí)的操作梳侨。一分析不打緊蛉威,發(fā)現(xiàn)問(wèn)題盡然出在了Linq語(yǔ)句上,這是為什么呢走哺?且聽(tīng)我娓娓道來(lái)蚯嫌。


使用Visual Studio Profiler進(jìn)行性能診斷

首先講一下如何使用VS自帶的性能分析工具(Visual Studio Profiler)進(jìn)行性能診斷,默認(rèn)是通過(guò)采樣(Sampling)的方式進(jìn)行性能分析丙躏≡袷荆可以具體根據(jù)實(shí)際情況,選擇性能分析方式晒旅。其他性能分析方式栅盲,詳細(xì)參考MSDN Visual Studio Profiler

第一步

添加需要監(jiān)控的程序集废恋、項(xiàng)目或者是網(wǎng)站


第二步
第三步

附加到進(jìn)程后谈秫,就會(huì)開(kāi)始進(jìn)行性能監(jiān)控。默認(rèn)是通過(guò)采樣(Sampling)的方式進(jìn)行性能分析鱼鼓。然后在應(yīng)用程序上進(jìn)行業(yè)務(wù)操作拟烫,操作結(jié)束后,點(diǎn)擊Stop Profiling迄本,就會(huì)生成性能報(bào)告硕淑。

采樣方式(Sampling)性能分析術(shù)語(yǔ)

Inclusive Samples(非獨(dú)占樣本數(shù)):執(zhí)行目標(biāo)函數(shù)期間收集的樣本總數(shù)。(包括執(zhí)行目標(biāo)函數(shù)和其子函數(shù)期間收集的樣本)
Exclusive Samples (獨(dú)占樣本數(shù)): 執(zhí)行目標(biāo)函數(shù)的指令期間收集的樣本總數(shù)。(不包含目標(biāo)函數(shù)調(diào)用的子函數(shù))
Hot Path(熱路徑):顯示收集數(shù)據(jù)時(shí)執(zhí)行最活躍的代碼路徑喜颁。
Functions Doing Most Individual Work:執(zhí)行單個(gè)工作最多的函數(shù)
Inclusive Samples %(非獨(dú)占樣本百分比): 數(shù)值越高說(shuō)明函數(shù)消耗整體資源越多
Exclusive Samples %(獨(dú)占樣本百分比): 數(shù)值越高說(shuō)明函數(shù)存在性能瓶頸曹阔。

看看我代碼執(zhí)行的性能分析報(bào)告


采樣分析報(bào)告

從圖中的Hot Path我們可以看到System.Linq.Enumerable.WhereEnumerableIterator`1.MoveNext()占用了最高的非獨(dú)占樣本百分比半开,說(shuō)明程序在這個(gè)地方有較高的資源消耗
對(duì).net熟悉的一看就知道這個(gè)方法是Linq的枚舉迭代器赃份。
那究竟性能瓶頸在哪呢寂拆?咱們來(lái)看看Functions Doing Most Individual Work占比最高的函數(shù)。
點(diǎn)開(kāi)具體的方法抓韩,可以清楚看到存在性能瓶頸的標(biāo)紅代碼段纠永。(Vs Profiler就是這么強(qiáng)大)

使用了Linq的匿名函數(shù)

使用了Linq的Any()函數(shù)
使用Linq的FirstOrDefault()

看完了性能分析報(bào)告,那就著手優(yōu)化吧谒拴。


知其然知其所以然尝江,為什么Linq會(huì)導(dǎo)致性能瓶頸

首先我們來(lái)看一個(gè)簡(jiǎn)單的Linq查詢代碼片段

class Symbol 
{ 
    public string Name { get; private set; } /*...*/
}
class Compiler 
{ 
    private List<Symbol> symbols; 
    public Symbol FindMatchingSymbol(string name) 
    { 
        return symbols.FirstOrDefault(s => s.Name == name); 
    }
}

為了展示FindMatchingSymbol(string name)函數(shù)其中的分配,我們首先將該單行函數(shù)拆分為兩行:

Func<Symbol, bool> predicate = s => s.Name == name; 
return symbols.FirstOrDefault(predicate);

第一行中英上,lambda表達(dá)式s=>s.Name==name” 是對(duì)本地變量name的一個(gè)閉包炭序。這就意味著需要分配額外的對(duì)象來(lái)為委托對(duì)象predict分配空間,需要一個(gè)分配一個(gè)靜態(tài)類來(lái)保存環(huán)境從而保存name的值苍日。編譯器會(huì)產(chǎn)生如下代碼:

// Compiler-generated class to hold environment state for lambda 
private class Lambda1Environment 
{ 
    public string capturedName; 
    public bool Evaluate(Symbol s) 
    { 
        return s.Name == this.capturedName;
    } 
}

// Expanded Func<Symbol, bool> predicate = s => s.Name == name; 
Lambda1Environment l = new Lambda1Environment() 
{ 
    capturedName = name
}; 
var predicate = new Func<Symbol, bool>(l.Evaluate);

兩個(gè)new操作符(第一個(gè)創(chuàng)建一個(gè)環(huán)境類惭聂,第二個(gè)用來(lái)創(chuàng)建委托)很明顯的表明了內(nèi)存分配的情況。
現(xiàn)在來(lái)看看FirstOrDefault方法的調(diào)用相恃,他是IEnumerable<T>類的擴(kuò)展方法辜纲,這也會(huì)產(chǎn)生一次內(nèi)存分配。因?yàn)?strong>FirstOrDefault使用IEnumerable<T>作為第一個(gè)參數(shù)拦耐,可以將上面的展開(kāi)為下面的代碼:

// Expanded return symbols.FirstOrDefault(predicate) ... 
IEnumerable<Symbol> enumerable = symbols;
IEnumerator<Symbol> enumerator = enumerable.GetEnumerator(); 
while (enumerator.MoveNext())
{ 
    if (predicate(enumerator.Current)) 
        return enumerator.Current; 
} 
return default(Symbol);

symbols變量是類型為List<T>的變量耕腾。List<T>集合類型實(shí)現(xiàn)了IEnumerable<T>即可并且清晰地定義了一個(gè)迭代器List<T>的迭代器使用了一種結(jié)構(gòu)體來(lái)實(shí)現(xiàn)杀糯。使用結(jié)構(gòu)而不是類意味著通秤牡耍可以避免任何在托管堆上的分配,從而可以影響垃圾回收的效率火脉。枚舉典型的用處在于方便語(yǔ)言層面上使用foreach循環(huán)牵舵,他使用enumerator結(jié)構(gòu)體在調(diào)用推棧上返回。遞增調(diào)用堆棧指針來(lái)為對(duì)象分配空間倦挂,不會(huì)影響GC對(duì)托管對(duì)象的操作畸颅。
在上面的展開(kāi)FirstOrDefault調(diào)用的例子中,代碼會(huì)調(diào)用IEnumerabole<T>接口中的GetEnumerator()方法方援。將symbols賦值給IEnumerable<Symbol>類型的enumerable變量没炒,會(huì)使得對(duì)象丟失了其實(shí)際的List<T>類型信息。這就意味著當(dāng)代碼通過(guò)enumerable.GetEnumerator()方法獲取迭代器時(shí)犯戏,.NET Framework 必須對(duì)返回的值(即迭代器送火,使用結(jié)構(gòu)體實(shí)現(xiàn))類型進(jìn)行裝箱從而將其賦給IEnumerable<Symbol>類型的(引用類型)enumerator變量拳话。
解決方法:
解決辦法是重寫FindMatchingSymbol方法,將單個(gè)語(yǔ)句使用六行代碼替代种吸,這些代碼依舊連貫弃衍,易于閱讀和理解,也很容易實(shí)現(xiàn)坚俗。

public Symbol FindMatchingSymbol(string name) 
{ 
    foreach (Symbol s in symbols)
    { 
        if (s.Name == name) 
            return s; 
    } 
    return null; 
}

代碼中并沒(méi)有使用LINQ擴(kuò)展方法镜盯,lambdas表達(dá)式和迭代器,并且沒(méi)有額外的內(nèi)存分配開(kāi)銷猖败。這是因?yàn)榫幾g器看到symbolList<T>類型的集合速缆,因?yàn)槟軌蛑苯訉⒎祷氐慕Y(jié)構(gòu)性的枚舉器綁定到類型正確的本地變量上,從而避免了對(duì)struct類型的裝箱操作恩闻。原先的代碼展示了C#語(yǔ)言豐富的表現(xiàn)形式以及.NET Framework 強(qiáng)大的生產(chǎn)力艺糜。改后的代碼則更加高效簡(jiǎn)單,并沒(méi)有添加復(fù)雜的代碼而增加可維護(hù)性幢尚。


看完以上分析是不是覺(jué)得不可思議倦踢,我們簡(jiǎn)單的一個(gè)Linq語(yǔ)句最終會(huì)讓編譯器做那么多繁瑣的工作。

針對(duì)以上分析侠草,對(duì)代碼進(jìn)行優(yōu)化相應(yīng)優(yōu)化:

代碼優(yōu)化1
代碼優(yōu)化2

優(yōu)化后的采樣分析報(bào)告可以看出System.Linq.Enumerable.WhereEnumerableIterator`1.MoveNext()的占比從68%降低到了13%辱挥。已經(jīng)大大的優(yōu)化了程序中Linq存在的性能問(wèn)題。根據(jù)實(shí)際測(cè)試結(jié)果边涕,耗時(shí)優(yōu)化已經(jīng)降低了一半以上晤碘,已經(jīng)達(dá)到了此次代碼優(yōu)化的目的。

優(yōu)化后采樣分析報(bào)告

到這里針對(duì)Linq的性能優(yōu)化就結(jié)束了功蜓≡耙可能讀者還會(huì)對(duì)最終的采樣分析報(bào)告有疑問(wèn),明明還有幾個(gè)點(diǎn)占比很高啊式撼,為什么不繼續(xù)優(yōu)化童社?
那是因?yàn)槭O碌牟蓸勇识际菢I(yè)務(wù)邏輯相關(guān)的,只能從業(yè)務(wù)邏輯上著手優(yōu)化了著隆。

本文主要參考自.NET程序的性能要領(lǐng)和優(yōu)化建議

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末扰楼,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子美浦,更是在濱河造成了極大的恐慌弦赖,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,039評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件浦辨,死亡現(xiàn)場(chǎng)離奇詭異蹬竖,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門币厕,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)列另,“玉大人,你說(shuō)我怎么就攤上這事旦装∫逞茫” “怎么了?”我有些...
    開(kāi)封第一講書人閱讀 165,417評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵同辣,是天一觀的道長(zhǎng)拷姿。 經(jīng)常有香客問(wèn)我惭载,道長(zhǎng)旱函,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 58,868評(píng)論 1 295
  • 正文 為了忘掉前任描滔,我火速辦了婚禮棒妨,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘含长。我一直安慰自己券腔,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,892評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布拘泞。 她就那樣靜靜地躺著纷纫,像睡著了一般。 火紅的嫁衣襯著肌膚如雪陪腌。 梳的紋絲不亂的頭發(fā)上辱魁,一...
    開(kāi)封第一講書人閱讀 51,692評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音诗鸭,去河邊找鬼染簇。 笑死,一個(gè)胖子當(dāng)著我的面吹牛强岸,可吹牛的內(nèi)容都是我干的锻弓。 我是一名探鬼主播,決...
    沈念sama閱讀 40,416評(píng)論 3 419
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼蝌箍,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼青灼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起妓盲,我...
    開(kāi)封第一講書人閱讀 39,326評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤聚至,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后本橙,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體扳躬,經(jīng)...
    沈念sama閱讀 45,782評(píng)論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,957評(píng)論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了贷币。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片击胜。...
    茶點(diǎn)故事閱讀 40,102評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖役纹,靈堂內(nèi)的尸體忽然破棺而出偶摔,到底是詐尸還是另有隱情,我是刑警寧澤促脉,帶...
    沈念sama閱讀 35,790評(píng)論 5 346
  • 正文 年R本政府宣布辰斋,位于F島的核電站,受9級(jí)特大地震影響瘸味,放射性物質(zhì)發(fā)生泄漏宫仗。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,442評(píng)論 3 331
  • 文/蒙蒙 一旁仿、第九天 我趴在偏房一處隱蔽的房頂上張望藕夫。 院中可真熱鬧,春花似錦枯冈、人聲如沸毅贮。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 31,996評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)滩褥。三九已至,卻和暖如春炫加,著一層夾襖步出監(jiān)牢的瞬間瑰煎,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 33,113評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工琢感, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留丢间,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,332評(píng)論 3 373
  • 正文 我出身青樓驹针,卻偏偏與公主長(zhǎng)得像烘挫,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子柬甥,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,044評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • 版本記錄 前言 我們?cè)谧鯽pp的時(shí)候饮六,不是做完功能就結(jié)束了,很多時(shí)候是需要進(jìn)行檢查和優(yōu)化的苛蒲,而xcode自帶了一個(gè)...
    刀客傳奇閱讀 2,796評(píng)論 0 1
  • 1. 引言 最近一段時(shí)間卤橄,系統(tǒng)新版本要發(fā)布,在beta客戶測(cè)試期間臂外,暴露了很多問(wèn)題窟扑,除了一些業(yè)務(wù)和異常問(wèn)題外喇颁,其他...
    圣杰閱讀 1,208評(píng)論 2 18
  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,167評(píng)論 25 707
  • 機(jī)器學(xué)習(xí)是做NLP和計(jì)算機(jī)視覺(jué)這類應(yīng)用算法的基礎(chǔ),雖然現(xiàn)在深度學(xué)習(xí)模型大行其道嚎货,但是懂一些傳統(tǒng)算法的原理和它們之間...
    在河之簡(jiǎn)閱讀 20,507評(píng)論 4 65
  • 背景 關(guān)于python的導(dǎo)入的原因一直不是很理解橘霎,上網(wǎng)查了一下這個(gè)與命名空間有關(guān)。所以寫了這樣一篇博客梳理其關(guān)系殖属。...
    shawnxjf閱讀 741評(píng)論 0 2