记一次。NET程序的性能优化实战(3)深入。NET源码
前言
前两篇文章 part1 和 part2 基本上理清了 IsSplitter() 运行缓慢的原因 —— 在函数内部使用了带 Compile 选项的正则表达式。
但是没想到在 IsSplitter() 内部使用不带 Compiled 选项的正则表达式,整个程序运行起来非常快,跟静态函数版本的运行速度不相上下。又有了如下疑问:为什么使用不带 Compiled 选项实例化的 Regex 速度会这么快?为什么把 Regex 变量从局部改成全局变量后运行速度有了极大提升?除了避免重复实例化,还有哪些提升?为什么 PerfView 收集到的采样数据,大部分发生在 MatchCollections.Count 内部,极少发生在 Regex 的构造函数内部?(使用带 Compiled 选项的正则表达式的时候)Regex.IsMatch() 是如何使用缓存的?直接实例化的 Regex 对象会使用正则表达式引擎内部的缓存吗?正则表达式引擎内部根据什么缓存的? 什么时候会生成动态方法?生成的动态方法是在哪里调用的?
本文会继续使用 Perfview 抓取一些关键数据进行分析,有些疑问需要到 .NET 源码中寻找答案。在查看代码的过程中,发现有些逻辑单纯看源码不太容易理解,于是又调试跟踪了 .NET 中正则表达式相关源码。由于篇幅原因,本篇不会介绍如何下载 .NET 源码,如何调试 .NET 源码的方法。但是会单独写一篇简单的介绍文章 。解惑为什么使用不带 Compiled 选项实例化的 Regex 速度会这么快?还是使用 PerfView 采集性能数据并分析,如下图:可以发现, IsSplitter() 函数只在第一次被调用时发生了一次 JIT ,后续调用耗时不到 0.1ms (图中最后一次调用耗时:4090.629-4090.597 = 0.032ms )。使用带 Compiled 选项实例化的 Regex 的 IsSplitter() 函数,如下图:view-filter-event-with-etwlogger 每次调用大概要消耗 11ms (5616.375 - 5604.637 = 11.738 ms )。至于为什么不带 Compiled 选项的正则表达式在调用过程中没有多余的 JIT ,与疑问7 一起到源码中找答案。为什么把 Regex 变量从局部改成全局变量后运行速度有了极大提升?除了避免重复实例化,还有哪些提升?修改代码,把局部变量改成全局变量,编译。再次使用 PerfView 采集性能数据并分析,如下图:可以发现与使用不带 Compiled 选项的局部变量版本一样,只发生了一次 JIT 。所以把局部变量改成全局变量后,除了避免了重复实例化的开销(很小),更重要的是避免了多余的 JIT 操作。为什么 PerfView 收集到的采样数据,大部分发生在 MatchCollections.Count 内部,极少发生在 Regex 的构造函数内部?(使用带 Compiled 选项的正则表达式的时候)Regex 构造函数只被 JIT 了一次,后面的调用都是在执行原生代码,执行速度非常快。而 MatchCollections.Count 每次执行的时候都需要执行 JIT (每次都需要 10ms 以 上),所以大部分数据在 MatchCollections.Count 内部,是非常合理的。Regex.IsMatch() 是如何使用缓存的?Regex.IsMatch() 有很多重载版本,最后都会调用下面的版本:static bool IsMatch (String input, String pattern, RegexOptions options, TimeSpan matchTimeout) {
return new Regex(pattern, options, matchTimeout, true ).IsMatch(input);
}
该函数会在内部构造一个临时的 Regex 对象,并且构造函数的最后一个参数 useCaChe 的值是 true ,表示使用缓存。
疑问5 和 疑问6 的答案在 Regex 的构造函数中,先看看 Regex 的构造函数。Regex 构造函数
Regex 有很多个构造函数,列举如下: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) {}
注意: 以上构造函数的最后一个参数都是 false ,表示不使用缓存。
这些构造函数最后都会调用下面的 私有 构造函数(代码有所精简调整): 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(); // 构造缓存用到的 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,包含了字节码等信息 RegexTree tree = RegexParser.Parse(pattern, roptions); code = RegexWriter.Write(tree); // 如果指定了 useCache 参数就缓存起来,下次就能在缓存中找到了 if (useCache) cached = CacheCode(key); } else { // 如果找到了缓存就使用缓存中的信息 code = cached._code; factory = cached._factory; runnerref = cached._runnerref; } // 如果指定了 Compiled 选项,并且 factory 是空(没使用缓存,或者缓存中的 _factory 是空) if (UseOptionC() && factory == null) { // 根据 code 和 roptions 生成 factory factory = Compile(code, roptions); // 需要缓存就缓存起来 if (useCache && cached != null) cached.AddCompiled(factory); } }
注意: 带 bool useCache 标记的构造函数是私有的,也就是说不能直接使用此构造函数实例化 Regex 。
首先会根据 option + culture + pattern 到缓存中查找。如果没找到缓存就生成类型为 RegexCodes 的 code (包含了字节码等信息),如果找到了缓存就使用缓存中的信息。 如果指定了 Compiled 选项(UseOptionC() 会返回 true ),并且 factory 是空(没使用缓存或者缓存中的 _factory 是空),就会执行 Compile() 函数,并把返回值保存到 factory 成员中。
至此,可以回答第 5 6 两个疑问了。直接实例化的 Regex 对象会使用正则表达式引擎内部的缓存吗?会优先根据 option + culture + pattern 到缓存中查找 ,但是否更新缓存 是由最后一个参数 useCache 决定的,与是否指定 Compiled 选项无关。正则表达式引擎内部根据什么缓存的? 根据 option + culture + pattern 缓存。
疑问7 与由 疑问1 引申出来的 JIT 问题是一个问题。之所以会 JIT ,是因为有需要 JIT 的代码,如果不断有新的动态方法产生出来并执行,那么就需要不断地 JIT 。由于此问题涉及到的代码量比较大,逻辑比较复杂,需要深入 .NET 源码进行查看。为了更好的理解整个过程,我简单梳理了 IsSpitter() 函数中涉及到的关键类以及类之间的关系,整理成下图,供参考。流程 & 类关系梳理
看完上图后,可以继续看剩下的 JIT 问题了。因为大多数 JIT 都出现在 MatchCollection.Count 中,可以由此切入。MatchCollection.Count
实现代码如下: public int Count { get { if (_done) return _matches.Count; GetMatch(infinite); return _matches.Count; } }
Count 会调用 GetMatch() 函数,而 GetMatch() 函数会不断调用 _regex.Run() 函数。
_regex 是哪来的呢?在构造 MatchCollection 实例时传过来的。
MatchCollection 是由 Regex.Matches() 实例化的,代码如下(去掉了判空逻辑):public MatchCollection Matches(String input, int startat) { return new MatchCollection(this, input, 0, input.Length, startat); }
该函数会实例化一个 MatchCollection 对象,并把当前 Regex 实例作为第一个参数传给 MatchCollection 的构造函数。该参数会被保存到 MatchCollection 实例的 _regex 成员中。
接下来继续查看 Regex.Run 函数的实现。Regex.Run()
具体实现代码如下(代码有精简): internal Match Run(bool quick, int prevlen, String input, int beginning, int length, int startat) { Match match; // 使用缓存的时候,可能从缓存中拿到一个有效的 runner,其它情况下都是 null。 RegexRunner runner = (RegexRunner)runnerref.Get(); // 不使用缓存的时候 runner是 null if (runner == null) { // 如果 factory 不为空就通过 factory 创建一个 runner。 // 使用了 Compiled 标志创建的 Regex 实例的 factory 不为空 if (factory != null) runner = factory.CreateInstance(); else runner = new RegexInterpreter(code, UseOptionInvariant() ? CultureInfo.InvariantCulture : CultureInfo.CurrentCulture); } try { // 调用 RegexRunner.Scan 扫描匹配项。 match = runner.Scan(this, input, beginning, beginning + length, startat, prevlen, quick, internalMatchTimeout); } finally { runnerref.Release(runner); } return match; }
逻辑还是非常清晰的,先找到或者创建(通过 factory.CreateInstance() 或者直接 new )一个类型为 RegexRunner 实例 runner ,然后调用 runner->Scan() 进行匹配。
对于使用 Compiled 选项创建的 Regex ,其 factory 成员变量会在 Regex 构造函数中赋值,对应的语句是 factory = Compile(code, roptions); ,类型是 CompiledRegexRunnerFactory 。
我们先来看看 CompiledRegexRunnerFactory.CreateInstance() 的实现。CompiledRegexRunnerFactory.CreateInstance()
代码如下: protected internal override RegexRunner CreateInstance() { CompiledRegexRunner runner = new CompiledRegexRunner(); new ReflectionPermission(PermissionState.Unrestricted).Assert(); // 设置关键的动态函数,这三个函数是在 `RegexLWCGCompiler` // 类的 `FactoryInstanceFromCode()` 中生成的。 runner.SetDelegates( (NoParamDelegate) goMethod.CreateDelegate(typeof(NoParamDelegate)), (FindFirstCharDelegate) findFirstCharMethod.CreateDelegate(typeof(FindFirstCharDelegate)), (NoParamDelegate) initTrackCountMethod.CreateDelegate(typeof(NoParamDelegate)) ); return runner; }
该函数返回的是 CompiledRegexRunner 类型的 runner 。在返回之前会先调用 runner.SetDelegates 为对应的关键函数(Go , FindFirstChar , InitTrackCount )赋值。参数中的 goMethod, findFirstCharMethod, initTrackCountMethod 是在哪里赋值的呢?在 Regex.Compile() 函数中赋值的。Regex.Compile()
Regex.Compile() 会直接转调 RegexCompiler 的静态函数 Compile() ,相关代码如下(有调整):internal static RegexRunnerFactory Compile(RegexCode code, RegexOptions options) { RegexLWCGCompiler c = new RegexLWCGCompiler(); return c.FactoryInstanceFromCode(code, options); }
该函数直接调用了 RegexLWCGCompiler 类的 FactoryInstanceFromCode() 成员函数。相关代码如下(有删减):internal RegexRunnerFactory FactoryInstanceFromCode(RegexCode code, RegexOptions options) { // 获取唯一标识符,也就是FindFirstChar后面的数字 int regexnum = Interlocked.Increment(ref _regexCount); string regexnumString = regexnum.ToString(CultureInfo.InvariantCulture); // 生成动态函数Go DynamicMethod goMethod = DefineDynamicMethod("Go" + regexnumString, null, typeof(CompiledRegexRunner)); GenerateGo(); // 生成动态函数FindFirstChar DynamicMethod firstCharMethod = DefineDynamicMethod("FindFirstChar" + regexnumString, typeof(bool), typeof(CompiledRegexRunner)); GenerateFindFirstChar(); // 生成动态函数InitTrackCount DynamicMethod trackCountMethod = DefineDynamicMethod("InitTrackCount" + regexnumString, null, typeof(CompiledRegexRunner)); GenerateInitTrackCount(); return new CompiledRegexRunnerFactory(goMethod, firstCharMethod, trackCountMethod); }
该函数非常清晰易懂,但却是 非常关键 的一个函数,会生成三个动态函数(也就是通过 PerfView 采集到的 FindFirstCharXXX ,GoXXX ,InitTrackCountXXX ),最后会构造一个类型为 CompiledRegexRunnerFactory 的实例,并把生成的动态函数作为参数传递给 CompiledRegexRunnerFactory 的构造函数。
至此,已经找到 生成动态函数 的地方了。动态函数是什么时候被调用的呢?在 runner.Scan() 函数中被调用的。RegexRunner.Scan()
关键代码如下(做了大量删减): 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() 函数内部会调用 FindFirstChar() 和 Go() ,而且只有当 FindFirstChar() 返回 true 的时候,才会调用 Go() 。这两个函数是虚函数,具体的子类会重写。对于 Compiled 类型的正则表达式,对应的 runner 类型是 CompiledRegexRunner 。这三个关键的函数实现如下: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); } }
现在可以回答 疑问7 及 疑问1 引申出来的 JIT 问题了。什么时候会生成动态方法?生成的动态方法是在哪里调用的? 在指定了 Compiled 标志的 Regex 的构造函数内部会调用 RegexCompiler.Compile() 函数,Compile() 函数又会调用 RegexLWCGCompiler.FactoryInstanceFromCode() ,FactoryInstanceFromCode() 函数内部会分别调用 GenerateFindFirstChar() , GenerateGo() , GenerateInitTrackCount() 生成对应的动态方法。在执行 MatchCollection.Count 的时候,会调用 MatchCollection.GetMatch() 函数,GetMatch() 函数会调用对应 RegexRunner 的 Scan() 函数。Scan() 函数会调用 RegexRunner.FindFirstChar() ,而 CompiledRegexRunner 类型中的 FindFirstChar() 函数调用的是设置好的动态函数。Compiled 与 非 Compiled 对比1. 构造函数
带 Compiled 选项的 Regex
useCache 传递的是 false ,表示不使用缓存。因为指定了 RegexOptions.Compiled 选项, Regex 的构造函数内部会调用 RegexCompiler.Compile() 函数,Compile() 函数又会调用 RegexLWCGCompiler.FactoryInstanceFromCode() ,FactoryInstanceFromCode() 函数内部会分别调用 GenerateFindFirstChar() , GenerateGo() , GenerateInitTrackCount() 生成对应的动态方法,然后返回 CompiledRegexRunnerFactory 类型的实例。如下图:
compiled-regex-constructor
不带 Compiled 选项的 Regex
构造函数与 Compiled 的基本一致,useCache 传递的也是 false ,不使用缓存。因为 UseOptionC() 返回的是 false ,所以不会执行 Compile() 函数。所以 factory 成员变量是 null 。
这里就不贴图了。 2. matches.Count
带 Compiled 选项的 Regex
MatchCollection-count-dynamic-FindFirstChar
MatchCollection.Count 内部会调用 GetMatch() 函数,GetMatch() 函数会调用对应 RegexRunner 的 Scan() 函数(这里的 runner 类型是 CompiledRegexRunner )。Scan() 内部会调用 FindFirstChar() 函数,而 CompiledRegexRunner 类型的 FindFirstChar() 函数内部调用的是设置好的动态方法。
不带 Compiled 选项的 Regex
MatchCollection-count-none-dynamic-FindFirstChar
与带 Compiled 版本的调用栈基本一致,不一样的是这里 runner 的类型是 RegexInterpreter ,该类型的 FindFirstChar() 函数调用的代码不是动态生成的。3. runner 赋值
当 runner 是 null 的时候,需要根据情况获取对应的 runner 。
带 Compiled 选项的 Regex
factory 成员在 Regex 构造函数里通过 Compile() 赋过值,runner 会通过下图 1306 行的 factory.CreateInstance() 赋值。
不带 Compiled 选项的 Regex
factory 成员没有被赋过值,因此是空的,runner 会通过下图 1308 行的 new RegexInterpreter() 赋值。
runner 总结不要在循环内部创建编译型的正则表达式(带 Compiled 选项),会频繁导致 JIT 的发生进而影响效率。Regex.IsMatch() 也会创建 Regex 实例,但是最后一个参数 bUseCache 是 true ,表示使用缓存。Regex 构造函数的最后一个参数 bUseCache 是 true 的时候才会更新缓存。正则表达式引擎内部会根据 option + culture + pattern 查找缓存。参考资料
.NET源码
新规改造满6个月信用卡助力消费复苏本报记者张漫游北京报道2022年7月关于进一步促进信用卡业务规范健康发展的通知(以下简称通知或信用卡新规)落地,其中提到,各银行信用卡部门需按照要求完成业务流程及系统改造等工作,期
共享合作发展新机遇来源人民网在老挝36庄园,技术人员在生产车间加工茶叶。老挝36庄园供图在西班牙马德里的一家超市,工作人员在工作。本报记者许海林摄巴西米奥罗葡萄酒庄园一瞥。鲁本斯卢西奥摄随着中国的对
天上掉下个大霹雷,但为何更该见猎心喜电影手机里面的经典台词,没事还是要打把伞的,不晓得哪片云彩有雨。本日上半场是众口相声吹牛逼时间,下午就立马遭遇雷劈电打,收盘数据显示,有600亿资金应声而逃。若要问为什么,没有什么
清远首笔数字人民币缴纳税费业务成功落地2月15日,忠华集团有限公司财务人员张小姐拿到了全市第一张使用数字人民币账户缴纳税款和非税收入的完税凭证,这标志着清远市首笔数字人民币缴纳税费业务成功落地。使用数字人民币缴纳税费,
收盘丨创业板指大跌2。51电信运营软件等板块跌幅居前2月17日,三大股指低开低走,创业板指走势较弱。截至收盘,沪指跌0。77,深证成指跌1。61,创业板指跌2。51。总体上个股跌多涨少,两市超2800只个股下跌。沪深两市今日成交额9
美国不再是百万富翁定居首选英国调查报告显示,美国不再是百万富翁的首选移居国家。图为美国富豪度假胜地迈阿密棕榈滩。(彭博社)(纽约彭博电)美国曾经是百万富翁的首选移居国家,但如今美国却失去了这份吸引力。英国投
中粮集团在宁夏银川投资30亿元布局现代养殖全产业链项目中新网银川2月17日电(记者李佩珊)日前,中粮集团与宁夏银川灵武市政府签订战略投资合作协议。中粮集团计划在灵武市投资30亿元,围绕当地现代养殖产业绿色食品加工产业布局规划,建设集饲
1000亿颗芯片订单,台积电重启南京工厂,中芯国际面临压力大家都知道台积电虽然是一家中国企业,但是一直受制于美国,因为台积电的技术人才专利大部分来源于美国,特别是光刻机的核心技术专利,并且美国拥有台积电20以上的股份,是第一大股东,台积电
(经济)山东文登外贸企业新春生产忙早春时节,山东省威海市文登区众多外贸企业开足马力赶制订单,当地政府制定出台多项优化营商环境政策,帮助企业解决用工融资等实际困难,助力企业实现2023年首季开门红。2月16日,员工在
山西振东制药股份有限公司技术中心被认定为国家企业技术中心2月3日,国家公布了2022年国家企业技术中心名单,山西振东制药股份有限公司技术中心被认定为国家企业技术中心,是山西省2022年唯一通过国家认定的企业技术中心。国家企业技术中心是由
全力以赴领航未来珍岛集团获评TMA年度最具影响力移动营销公司1月6日,珍岛集团经过专家评审团策略创意媒介应用技术支持效果转化等多个角度的考核,获选第九届TMA(TopMobileAwards)移动营销大奖年度最具影响力移动营销公司,成为推动