最近工作上遇到了一個(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)告
從圖中的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)大)
看完了性能分析報(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器看到symbol是List<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)化后的采樣分析報(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)化的目的。
到這里針對(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)化建議