記一次 .NET 程序的性能優(yōu)化實(shí)戰(zhàn)(3)—— 深入 .NET 源碼
前言
前兩篇文章 part1 和 part2 基本上理清了 IsSplitter() 運(yùn)行緩慢的原因 —— 在函數(shù)內(nèi)部使用了帶 Compile 選項(xiàng)的正則表達(dá)式。
但是沒想到在 IsSplitter() 內(nèi)部使用不帶 Compiled 選項(xiàng)的正則表達(dá)式,整個(gè)程序運(yùn)行起來非常快,跟靜態(tài)函數(shù)版本的運(yùn)行速度不相上下。又有了如下疑問:
為什么使用不帶 Compiled選項(xiàng)實(shí)例化的Regex速度會(huì)這么快?為什么把 ? Regex變量從局部改成全局變量后運(yùn)行速度有了極大提升?除了避免重復(fù)實(shí)例化,還有哪些提升?為什么 PerfView收集到的采樣數(shù)據(jù),大部分發(fā)生在MatchCollections.Count內(nèi)部,極少發(fā)生在Regex的構(gòu)造函數(shù)內(nèi)部?(使用帶Compiled選項(xiàng)的正則表達(dá)式的時(shí)候)Regex.IsMatch()是如何使用緩存的?直接實(shí)例化的 Regex對(duì)象會(huì)使用正則表達(dá)式引擎內(nèi)部的緩存嗎?正則表達(dá)式引擎內(nèi)部根據(jù)什么緩存的? 什么時(shí)候會(huì)生成動(dòng)態(tài)方法?生成的動(dòng)態(tài)方法是在哪里調(diào)用的?
本文會(huì)繼續(xù)使用 Perfview 抓取一些關(guān)鍵數(shù)據(jù)進(jìn)行分析,有些疑問需要到 .NET 源碼中尋找答案。在查看代碼的過程中,發(fā)現(xiàn)有些邏輯單純看源碼不太容易理解,于是又調(diào)試跟蹤了 .NET 中正則表達(dá)式相關(guān)源碼。由于篇幅原因,本篇不會(huì)介紹如何下載 .NET 源碼,如何調(diào)試 .NET 源碼的方法。但是會(huì)單獨(dú)寫一篇簡(jiǎn)單的介紹文章 。
解惑
為什么使用不帶
Compiled選項(xiàng)實(shí)例化的Regex速度會(huì)這么快?還是使用
PerfView采集性能數(shù)據(jù)并分析,如下圖:
可以發(fā)現(xiàn),
IsSplitter()函數(shù)只在第一次被調(diào)用時(shí)發(fā)生了一次JIT,后續(xù)調(diào)用耗時(shí)不到0.1ms(圖中最后一次調(diào)用耗時(shí):4090.629-4090.597 = 0.032ms)。使用帶
Compiled選項(xiàng)實(shí)例化的Regex的IsSplitter()函數(shù),如下圖:
view-filter-event-with-etwlogger 每次調(diào)用大概要消耗
11ms(5616.375 - 5604.637 = 11.738 ms)。至于為什么不帶
Compiled選項(xiàng)的正則表達(dá)式在調(diào)用過程中沒有多余的JIT,與疑問7一起到源碼中找答案。為什么把 ?
Regex變量從局部改成全局變量后運(yùn)行速度有了極大提升?除了避免重復(fù)實(shí)例化,還有哪些提升?修改代碼,把局部變量改成全局變量,編譯。再次使用
PerfView采集性能數(shù)據(jù)并分析,如下圖:
可以發(fā)現(xiàn)與使用不帶
Compiled選項(xiàng)的局部變量版本一樣,只發(fā)生了一次JIT。所以把局部變量改成全局變量后,除了避免了重復(fù)實(shí)例化的開銷(很?。?,更重要的是避免了多余的JIT操作。為什么
PerfView收集到的采樣數(shù)據(jù),大部分發(fā)生在MatchCollections.Count內(nèi)部,極少發(fā)生在Regex的構(gòu)造函數(shù)內(nèi)部?(使用帶Compiled選項(xiàng)的正則表達(dá)式的時(shí)候)Regex構(gòu)造函數(shù)只被JIT了一次,后面的調(diào)用都是在執(zhí)行原生代碼,執(zhí)行速度非???。而MatchCollections.Count每次執(zhí)行的時(shí)候都需要執(zhí)行JIT(每次都需要10ms以 上),所以大部分?jǐn)?shù)據(jù)在MatchCollections.Count內(nèi)部,是非常合理的。Regex.IsMatch()是如何使用緩存的?Regex.IsMatch()有很多重載版本,最后都會(huì)調(diào)用下面的版本:static?bool?IsMatch(String?input,?String?pattern,?RegexOptions?options,?TimeSpan?matchTimeout)?{
??return?new?Regex(pattern,?options,?matchTimeout,?true).IsMatch(input);
}該函數(shù)會(huì)在內(nèi)部構(gòu)造一個(gè)臨時(shí)的
Regex對(duì)象,并且構(gòu)造函數(shù)的最后一個(gè)參數(shù)useCaChe的值是true,表示使用緩存。
疑問5 和 疑問6 的答案在 Regex 的構(gòu)造函數(shù)中,先看看 Regex 的構(gòu)造函數(shù)。
Regex 構(gòu)造函數(shù)
Regex 有很多個(gè)構(gòu)造函數(shù),列舉如下:
public?Regex(String?pattern)
??:?this(pattern,?RegexOptions.None,?DefaultMatchTimeout,?false)?{}
??
public?Regex(String?pattern,?RegexOptions?options)
??:?this(pattern,?options,?DefaultMatchTimeout,?false)?{}
????????
Regex(String?pattern,?RegexOptions?options,?TimeSpan?matchTimeout)
??:?this(pattern,?options,?matchTimeout,?false)?{}
注意: 以上構(gòu)造函數(shù)的最后一個(gè)參數(shù)都是
false,表示不使用緩存。
這些構(gòu)造函數(shù)最后都會(huì)調(diào)用下面的私有構(gòu)造函數(shù)(代碼有所精簡(jiǎn)調(diào)整):
private?Regex(String?pattern,?RegexOptions?options,?TimeSpan?matchTimeout,?bool?useCache)
{
??string?cultureKey?=?null;
??if?((options?&?RegexOptions.CultureInvariant)?!=?0)
??????cultureKey?=?CultureInfo.InvariantCulture.ToString();?//?"English?(United?States)"
??else
??????cultureKey?=?CultureInfo.CurrentCulture.ToString();
??
??//?構(gòu)造緩存用到的?key,包含?options,culture?和?pattern
??String?key?=?((int)?options).ToString(NumberFormatInfo.InvariantInfo)?+?":"?+?cultureKey?+?":"?+?pattern;
??CachedCodeEntry?cached?=?LookupCachedAndUpdate(key);
??this.pattern?=?pattern;
??this.roptions?=?options;
??if?(cached?==?null)?{
??????//?如果沒找到緩存就生成類型為?RegexCodes?的?code,包含了字節(jié)碼等信息
??????RegexTree?tree?=?RegexParser.Parse(pattern,?roptions);
??????code?=?RegexWriter.Write(tree);
??????//?如果指定了?useCache?參數(shù)就緩存起來,下次就能在緩存中找到了
??????if?(useCache)
??????????cached?=?CacheCode(key);
??}?else?{
??????//?如果找到了緩存就使用緩存中的信息
??????code???????=?cached._code;
??????factory????=?cached._factory;
??????runnerref??=?cached._runnerref;
??}
??//?如果指定了?Compiled?選項(xiàng),并且?factory?是空(沒使用緩存,或者緩存中的?_factory?是空)
??if?(UseOptionC()?&&?factory?==?null)?{
??????//?根據(jù)?code?和?roptions?生成?factory
??????factory?=?Compile(code,?roptions);
??
??????//?需要緩存就緩存起來
??????if?(useCache?&&?cached?!=?null)
??????????cached.AddCompiled(factory);
??}
}
注意: 帶
bool useCache標(biāo)記的構(gòu)造函數(shù)是私有的,也就是說不能直接使用此構(gòu)造函數(shù)實(shí)例化Regex。
首先會(huì)根據(jù) option + culture + pattern 到緩存中查找。如果沒找到緩存就生成類型為 RegexCodes 的 code(包含了字節(jié)碼等信息),如果找到了緩存就使用緩存中的信息。如果指定了 Compiled 選項(xiàng)(UseOptionC() 會(huì)返回 true),并且 factory 是空(沒使用緩存或者緩存中的 _factory 是空),就會(huì)執(zhí)行 Compile() 函數(shù),并把返回值保存到 factory 成員中。
至此,可以回答第 5 6 兩個(gè)疑問了。
直接實(shí)例化的
Regex對(duì)象會(huì)使用正則表達(dá)式引擎內(nèi)部的緩存嗎?會(huì)優(yōu)先根據(jù)
option + culture + pattern到緩存中查找,但是否更新緩存是由最后一個(gè)參數(shù)useCache決定的,與是否指定Compiled選項(xiàng)無關(guān)。正則表達(dá)式引擎內(nèi)部根據(jù)什么緩存的?
根據(jù)
option + culture + pattern緩存。
疑問7 與由 疑問1 引申出來的 JIT 問題是一個(gè)問題。之所以會(huì) JIT,是因?yàn)橛行枰?JIT 的代碼,如果不斷有新的動(dòng)態(tài)方法產(chǎn)生出來并執(zhí)行,那么就需要不斷地 JIT。由于此問題涉及到的代碼量比較大,邏輯比較復(fù)雜,需要深入 .NET 源碼進(jìn)行查看。為了更好的理解整個(gè)過程,我簡(jiǎn)單梳理了 IsSpitter() 函數(shù)中涉及到的關(guān)鍵類以及類之間的關(guān)系,整理成下圖,供參考。
流程 & 類關(guān)系梳理

看完上圖后,可以繼續(xù)看剩下的 JIT 問題了。因?yàn)榇蠖鄶?shù) JIT 都出現(xiàn)在 MatchCollection.Count 中,可以由此切入。
MatchCollection.Count
實(shí)現(xiàn)代碼如下:
public?int?Count?{
??get?{
????if?(_done)
??????return?_matches.Count;
????GetMatch(infinite);
????return?_matches.Count;
??}
}
Count 會(huì)調(diào)用 GetMatch() 函數(shù),而 GetMatch() 函數(shù)會(huì)不斷調(diào)用 _regex.Run() 函數(shù)。
_regex 是哪來的呢?在構(gòu)造 MatchCollection 實(shí)例時(shí)傳過來的。
MatchCollection 是由 Regex.Matches() 實(shí)例化的,代碼如下(去掉了判空邏輯):
public?MatchCollection?Matches(String?input,?int?startat)?{
??return?new?MatchCollection(this,?input,?0,?input.Length,?startat);
}
該函數(shù)會(huì)實(shí)例化一個(gè) MatchCollection 對(duì)象,并把當(dāng)前 Regex 實(shí)例作為第一個(gè)參數(shù)傳給 MatchCollection 的構(gòu)造函數(shù)。該參數(shù)會(huì)被保存到 MatchCollection 實(shí)例的 _regex 成員中。
接下來繼續(xù)查看 Regex.Run 函數(shù)的實(shí)現(xiàn)。
Regex.Run()
具體實(shí)現(xiàn)代碼如下(代碼有精簡(jiǎn)):
internal?Match?Run(bool?quick,?int?prevlen,?String?input,?int?beginning,?int?length,?int?startat)?{
????Match?match;
????//?使用緩存的時(shí)候,可能從緩存中拿到一個(gè)有效的 runner,其它情況下都是 null。
????RegexRunner?runner?=?(RegexRunner)runnerref.Get();
????//?不使用緩存的時(shí)候?runner是?null
????if?(runner?==?null)?{
????????//?如果 factory 不為空就通過 factory 創(chuàng)建一個(gè) runner。
????????//?使用了?Compiled?標(biāo)志創(chuàng)建的?Regex?實(shí)例的?factory?不為空
????????if?(factory?!=?null)
????????????runner?=?factory.CreateInstance();
????????else
????????????runner?=?new?RegexInterpreter(code,?UseOptionInvariant()???CultureInfo.InvariantCulture?:?CultureInfo.CurrentCulture);
????}
????try?{
????????//?調(diào)用 RegexRunner.Scan 掃描匹配項(xiàng)。
????????match?=?runner.Scan(this,?input,?beginning,?beginning?+?length,?startat,?prevlen,?quick,?internalMatchTimeout);
????}?finally?{
????????runnerref.Release(runner);
????}
????return?match;
}
邏輯還是非常清晰的,先找到或者創(chuàng)建(通過 factory.CreateInstance() 或者直接 new)一個(gè)類型為 RegexRunner 實(shí)例 runner,然后調(diào)用 runner->Scan() 進(jìn)行匹配。
對(duì)于使用 Compiled 選項(xiàng)創(chuàng)建的 Regex,其 factory 成員變量會(huì)在 Regex 構(gòu)造函數(shù)中賦值,對(duì)應(yīng)的語(yǔ)句是 factory = Compile(code, roptions); ,類型是 CompiledRegexRunnerFactory。
我們先來看看 CompiledRegexRunnerFactory.CreateInstance() 的實(shí)現(xiàn)。
CompiledRegexRunnerFactory.CreateInstance()
代碼如下:
protected?internal?override?RegexRunner?CreateInstance()?{
??CompiledRegexRunner?runner?=?new?CompiledRegexRunner();
??new?ReflectionPermission(PermissionState.Unrestricted).Assert();
??//?設(shè)置關(guān)鍵的動(dòng)態(tài)函數(shù),這三個(gè)函數(shù)是在?`RegexLWCGCompiler`
??//?類的?`FactoryInstanceFromCode()`?中生成的。
??runner.SetDelegates(
????(NoParamDelegate)?goMethod.CreateDelegate(typeof(NoParamDelegate)),
????(FindFirstCharDelegate)?findFirstCharMethod.CreateDelegate(typeof(FindFirstCharDelegate)),
????(NoParamDelegate)?initTrackCountMethod.CreateDelegate(typeof(NoParamDelegate))
??);
??return?runner;
}
該函數(shù)返回的是 CompiledRegexRunner 類型的 runner。在返回之前會(huì)先調(diào)用 runner.SetDelegates 為對(duì)應(yīng)的關(guān)鍵函數(shù)(Go, FindFirstChar, InitTrackCount)賦值。參數(shù)中的 goMethod, findFirstCharMethod, initTrackCountMethod 是在哪里賦值的呢?在 Regex.Compile() 函數(shù)中賦值的。
Regex.Compile()
Regex.Compile() 會(huì)直接轉(zhuǎn)調(diào) RegexCompiler 的靜態(tài)函數(shù) Compile(),相關(guān)代碼如下(有調(diào)整):
internal?static?RegexRunnerFactory?Compile(RegexCode?code,?RegexOptions?options)?{
??RegexLWCGCompiler?c?=?new?RegexLWCGCompiler();
??return?c.FactoryInstanceFromCode(code,?options);
}
該函數(shù)直接調(diào)用了 RegexLWCGCompiler 類的 FactoryInstanceFromCode() 成員函數(shù)。相關(guān)代碼如下(有刪減):
internal?RegexRunnerFactory?FactoryInstanceFromCode(RegexCode?code,?RegexOptions?options)?{
??//?獲取唯一標(biāo)識(shí)符,也就是FindFirstChar后面的數(shù)字
??int?regexnum?=?Interlocked.Increment(ref?_regexCount);
??string?regexnumString?=?regexnum.ToString(CultureInfo.InvariantCulture);
??//?生成動(dòng)態(tài)函數(shù)Go
??DynamicMethod?goMethod?=?DefineDynamicMethod("Go"?+?regexnumString,?null,?typeof(CompiledRegexRunner));
??GenerateGo();
??//?生成動(dòng)態(tài)函數(shù)FindFirstChar
??DynamicMethod?firstCharMethod?=?DefineDynamicMethod("FindFirstChar"?+?regexnumString,?typeof(bool),?typeof(CompiledRegexRunner));
??GenerateFindFirstChar();
??
??//?生成動(dòng)態(tài)函數(shù)InitTrackCount??
??DynamicMethod?trackCountMethod?=?DefineDynamicMethod("InitTrackCount"?+?regexnumString,?null,?typeof(CompiledRegexRunner));
??GenerateInitTrackCount();
??return?new?CompiledRegexRunnerFactory(goMethod,?firstCharMethod,?trackCountMethod);
}
該函數(shù)非常清晰易懂,但卻是非常關(guān)鍵的一個(gè)函數(shù),會(huì)生成三個(gè)動(dòng)態(tài)函數(shù)(也就是通過 PerfView 采集到的 FindFirstCharXXX,GoXXX,InitTrackCountXXX),最后會(huì)構(gòu)造一個(gè)類型為 CompiledRegexRunnerFactory 的實(shí)例,并把生成的動(dòng)態(tài)函數(shù)作為參數(shù)傳遞給 CompiledRegexRunnerFactory 的構(gòu)造函數(shù)。
至此,已經(jīng)找到生成動(dòng)態(tài)函數(shù)的地方了。動(dòng)態(tài)函數(shù)是什么時(shí)候被調(diào)用的呢?在 runner.Scan() 函數(shù)中被調(diào)用的。
RegexRunner.Scan()
關(guān)鍵代碼如下(做了大量刪減):
Match?Scan(Regex?regex,?String?text,?int?textbeg,?int?textend,?int?textstart,?int?prevlen,?bool?quick,?TimeSpan?timeout)?{
??for?(;?;?)?{
????if?(FindFirstChar())?{
??????Go();
??????if?(runmatch._matchcount?[0]?>?0)
????????return?TidyMatch(quick);
????}
??}
}???????????????????????
可以看到,Scan() 函數(shù)內(nèi)部會(huì)調(diào)用 FindFirstChar() 和 Go(),而且只有當(dāng) FindFirstChar() 返回 true 的時(shí)候,才會(huì)調(diào)用 Go()。這兩個(gè)函數(shù)是虛函數(shù),具體的子類會(huì)重寫。對(duì)于 Compiled 類型的正則表達(dá)式,對(duì)應(yīng)的 runner 類型是 CompiledRegexRunner。這三個(gè)關(guān)鍵的函數(shù)實(shí)現(xiàn)如下:
internal?sealed?class?CompiledRegexRunner?:?RegexRunner?{
??NoParamDelegate?goMethod;
??FindFirstCharDelegate?findFirstCharMethod;
??NoParamDelegate?initTrackCountMethod;
????????
??protected?override?void?Go()?{
????goMethod(this);
??}
??protected?override?bool?FindFirstChar()?{
????return?findFirstCharMethod(this);
??}
??protected?override?void?InitTrackCount()?{
????initTrackCountMethod(this);
??}
}
現(xiàn)在可以回答疑問7 及疑問1 引申出來的 JIT 問題了。
什么時(shí)候會(huì)生成動(dòng)態(tài)方法?生成的動(dòng)態(tài)方法是在哪里調(diào)用的?
在指定了
Compiled標(biāo)志的Regex的構(gòu)造函數(shù)內(nèi)部會(huì)調(diào)用RegexCompiler.Compile()函數(shù),Compile()函數(shù)又會(huì)調(diào)用RegexLWCGCompiler.FactoryInstanceFromCode(),FactoryInstanceFromCode()函數(shù)內(nèi)部會(huì)分別調(diào)用GenerateFindFirstChar(),GenerateGo(),GenerateInitTrackCount()生成對(duì)應(yīng)的動(dòng)態(tài)方法。在執(zhí)行
MatchCollection.Count的時(shí)候,會(huì)調(diào)用MatchCollection.GetMatch()函數(shù),GetMatch()函數(shù)會(huì)調(diào)用對(duì)應(yīng)RegexRunner的Scan()函數(shù)。Scan()函數(shù)會(huì)調(diào)用RegexRunner.FindFirstChar(),而CompiledRegexRunner類型中的FindFirstChar()函數(shù)調(diào)用的是設(shè)置好的動(dòng)態(tài)函數(shù)。
Compiled 與 非 Compiled 對(duì)比
1. 構(gòu)造函數(shù)
帶 Compiled 選項(xiàng)的 Regex?
useCache 傳遞的是 false,表示不使用緩存。因?yàn)橹付?RegexOptions.Compiled 選項(xiàng), Regex 的構(gòu)造函數(shù)內(nèi)部會(huì)調(diào)用 RegexCompiler.Compile() 函數(shù),Compile() 函數(shù)又會(huì)調(diào)用 RegexLWCGCompiler.FactoryInstanceFromCode(),FactoryInstanceFromCode() 函數(shù)內(nèi)部會(huì)分別調(diào)用 GenerateFindFirstChar(), GenerateGo(), GenerateInitTrackCount() 生成對(duì)應(yīng)的動(dòng)態(tài)方法,然后返回 CompiledRegexRunnerFactory 類型的實(shí)例。如下圖:

不帶 Compiled 選項(xiàng)的 Regex?
構(gòu)造函數(shù)與 Compiled 的基本一致,useCache 傳遞的也是 false,不使用緩存。因?yàn)?UseOptionC() 返回的是 false,所以不會(huì)執(zhí)行 Compile() 函數(shù)。所以 factory 成員變量是 null。
這里就不貼圖了。
2. matches.Count
帶 Compiled 選項(xiàng)的 Regex?

MatchCollection.Count 內(nèi)部會(huì)調(diào)用 GetMatch() 函數(shù),GetMatch() 函數(shù)會(huì)調(diào)用對(duì)應(yīng) RegexRunner 的 Scan() 函數(shù)(這里的 runner 類型是 CompiledRegexRunner)。Scan() 內(nèi)部會(huì)調(diào)用 FindFirstChar() 函數(shù),而 CompiledRegexRunner 類型的 FindFirstChar() 函數(shù)內(nèi)部調(diào)用的是設(shè)置好的動(dòng)態(tài)方法。
不帶 Compiled 選項(xiàng)的 Regex?

與帶 Compiled 版本的調(diào)用?;疽恢拢灰粯拥氖沁@里 runner 的類型是 RegexInterpreter,該類型的 FindFirstChar() 函數(shù)調(diào)用的代碼不是動(dòng)態(tài)生成的。
3. runner 賦值
當(dāng) runner 是 null 的時(shí)候,需要根據(jù)情況獲取對(duì)應(yīng)的 runner。
帶 Compiled 選項(xiàng)的 Regex?
factory 成員在 Regex 構(gòu)造函數(shù)里通過 Compile() 賦過值,runner 會(huì)通過下圖 1306 行的 factory.CreateInstance() 賦值。
不帶 Compiled 選項(xiàng)的 Regex?
factory 成員沒有被賦過值,因此是空的,runner 會(huì)通過下圖 1308 行的 new RegexInterpreter() 賦值。

總結(jié)
不要在循環(huán)內(nèi)部創(chuàng)建編譯型的正則表達(dá)式(帶 Compiled選項(xiàng)),會(huì)頻繁導(dǎo)致JIT的發(fā)生進(jìn)而影響效率。Regex.IsMatch()也會(huì)創(chuàng)建 Regex 實(shí)例,但是最后一個(gè)參數(shù)bUseCache是true,表示使用緩存。Regex構(gòu)造函數(shù)的最后一個(gè)參數(shù)bUseCache是true的時(shí)候才會(huì)更新緩存。正則表達(dá)式引擎內(nèi)部會(huì)根據(jù) option + culture + pattern查找緩存。
參考資料
.NET源碼? ??https://referencesource.microsoft.com/
