前文回顾

Microsoft.Extensions.DependencyInjection 之一:解析实现 提到了 Microsoft.Extensions.DependencyInjection 包含以下核心组件。

IServiceCallSite

组件实例化上下文,包含许多实现,仅列举其中的ConstantCallSiteCreateInstanceCallSiteConstructorCallSite如下。

ConstructorCallSite既是IServiceCallSite的实现类,又复合引用IServiceCallSite,以表达自身参数的构建形式。

组件生命周期也使用IServiceCallSite表达,它们既从IServiceCallSite继承,也引用IServiceCallSite

CallSiteFactory

当组件需要被实例化时,CallSiteFactory从维护的ServiceDescriptor查找注入方式,对类型注入的组件使用反射解析其构造函数,并递归解析其参数,最后缓存得到的IServiceCallSite实例。

ServiceProviderEngine

ServiceProviderEngine是抽象类,内部依赖CallSiteRuntimeResolver完成基于反射的组件实例化,并缓存了组件实例化的委托。

CompiledServiceProviderEngine

CompiledServiceProviderEngineServiceProviderEngine继承,内部依赖ExpressionResolverBuilder完成基于表达式树的组件实例化的委托。

DynamicServiceProviderEngine

DynamicServiceProviderEngineCompiledServiceProviderEngine继承,它创建的委托比较特殊:

  • 该委托第1次执行实际是 ServiceProviderEngine 内部的CallSiteRuntimeResolver调用
  • 该委托第2次执行时开启异步任务,调用CompiledServiceProviderEngine内部的ExpressionResolverBuilder编译出委托并覆盖ServiceProviderEngine内部缓存。

为了印证该逻辑,这里使用 LINQPad 6 进行探索,该代码可以在控制台中运行,但部分语句仅在 LINQPad 中生效。

void Main() {
var services = new ServiceCollection()
.AddTransient<IFoo1, Foo1>()
.BuildServiceProvider();
var engine = ReflectionExtensions.GetNonPublicField(services, "_engine");
var realizedServices = (System.Collections.IDictionary)ReflectionExtensions.GetNonPublicProperty(engine, "RealizedServices"); for (int i = 0; i < 3; i++) {
services.GetRequiredService<IFoo1>(); //组件实例化
foreach (DictionaryEntry item in realizedServices) {
var title = String.Format("Loop {0}, type {1}, hash {2}", i, ((Type)item.Key).FullName, item.Value.GetHashCode());
item.Value.Dump(title, depth: 2); //仅被 LINQPad 支持
}
Thread.Sleep(10); //确保异步任务完成
}
} class ReflectionExtensions {
public static Object GetNonPublicField(Object instance, String name) {
var type = instance.GetType();
var field = type.GetField(name, BindingFlags.NonPublic | BindingFlags.Instance);
return field.GetValue(instance);
} public static Object GetNonPublicProperty(Object instance, String name) {
var type = instance.GetType();
var property = type.GetProperty(name, BindingFlags.NonPublic | BindingFlags.Instance);
return property.GetValue(instance);
}
} interface IFoo1 {
void Hello();
} class Foo1 : IFoo1 {
public void Hello() {
Console.WriteLine("Foo1.Hello()");
}
}

运行该脚本,可以看到

  • 第1次和第2次组件实例化,委托相同,hash 值都是 688136691,都是Microsoft.Extensions.DependencyInjection.ServiceLookup.DynamicServiceProviderEngine的内部委托;
  • 第1次和第2次组件实例化的 callSite计数从1谈到2;
  • 第3次组件实例例,委托变成了System.Object lambda_method(...),hash 值变成了 1561307880;

LINQPad 5 运行该脚本未能看到 hash 值变化,猜测是优化相关所致,考虑到DEBUG、单元测试和 LINQPad 6 已经实证,不再研究。

Microsoft.Extensions.DependencyInjection 之二:使用诊断工具观察内存占用 对比了组件实例化前后的内存变化如下图,从第3次开始组件实例化的性能大幅提升。

在以上基础上得到作了小结:

Microsoft.Extensions.DependencyInjection 并非是银弹,它的便利性是一种空间换时间的典型,我们需要对以下情况有所了解:

  • 重度使用依赖注入的大型项目启动过程相当之慢;
  • 如果单次请求需要实例化的组件过多,前期请求的内存开销不可轻视;
  • 由于实例化伴随着递归调用,过深的依赖将不可避免地导致堆栈溢出;

测试参数

Microsoft.Extensions.DependencyInjection 中抽象类Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngine有以下实现

  • Microsoft.Extensions.DependencyInjection.ServiceLookup.CompiledServiceProviderEngine
  • Microsoft.Extensions.DependencyInjection.ServiceLookup.DynamicServiceProviderEngine
  • Microsoft.Extensions.DependencyInjection.ServiceLookup.ExpressionsServiceProviderEngine
  • Microsoft.Extensions.DependencyInjection.ServiceLookup.RuntimeServiceProviderEngine
  • Microsoft.Extensions.DependencyInjection.ServiceLookup.ILEmitServiceProviderEngine

RuntimeServiceProviderEngine 是对 ServiceProviderEngine 的原样继承可以忽略,ExpressionsServiceProviderEngine CompiledServiceProviderEngine都是表达式树的使用也没有差异。ILEmitServiceProviderEngine是 emit 相关的实现。

aspnet/DependencyInjection 获取到的分支release/2.1 后,提取到ServiceProviderEngineCompiledServiceProviderEngineILEmitServiceProviderEngine等核心实现,再编写控制台程序,对依赖注入中反射、表达式树、emit 3种实现方式的开销和性能上进行探索。

程序使用启动参数控制组件实例化行为,并记录测试结果,以下是程序启动参数的解释。

-m|method

实例化方式,使用反射、表达式树与 Emit 参与了测试,分别对应:

  • ref:使用反射实例化组件,实质是对 CallSiteRuntimeResolver 的调用;
  • exp:使用表达式树实例化组件,实质是对 ExpressionResolverBuilder 的调用;
  • emit:使用 emit 实例化组件,实质是对 ILEmitResolverBuilder 的调用;
Action<Type, Boolean> handle = default;
if (method == "ref")
{
handle = GetRefService;
}
else if (method == "exp")
{
handle = GetExpService;
}
else if (method == "emit")
{
handle = GetEmitService;
}

-t|target

实例化目标,使用选取以下两种,配合参数 -n|number 使用

  • foo:使用 IFoo_{n} 作为实例化目标,已定义了 IFoo_0、IFoo_1、IFoo_2 至 IFoo_9999 共1万个接口与对应实现
  • bar:使用 IBar_{n} 作为实例化目标,只定义了 IBar_100、IBar_1000、IBar_5000、IBar_10000 共4个接口,每个实现均以 IFoo 作为构造函数的参数,
    • IBar_100:使用 IFoo_0、IFoo_1 至 IFoo_99 作为构造函数参数;
    • IBar_1000:使用 IFoo_0、IFoo_1 至 IFoo_999 作为构造函数参数;
    • IBar_5000:使用 IFoo_0、IFoo_1 至 IFoo_4999 作为构造函数参数;
    • IBar_10000:使用 IFoo_0、IFoo_1 至 IFoo_9999 作为构造函数参数;

该部分同 大量接口与实现类的生成 一样仍然使用脚本生成。

-n|number

帮助指示实例化的目标及数量

  • 100:target = foo 为从 IFoo_0、IFoo_1 至 IFoo_100 共100个接口,target =bar 则仅为 IBar_100;
  • 1000:target = foo 为从 IFoo_0、IFoo_1 至 IFoo_1000 共1000个接口,target = bar 则仅为 IBar_1000;
  • 5000:target = foo 为从 IFoo_0、IFoo_1 至 IFoo_5000 共5000个接口,target =bar 则仅为 IBar_5000;
  • 10000:target = foo 为从 IFoo_0、IFoo_1 至 IFoo_10000 共10000个接口,target =bar 则仅为 IBar_10000;

-c|cache

缓存行为,cache = false 时每次都构建委托,cache = true 则把构建委托缓存起来重复使用。GetRefService()实现如下,GetExpService()GetEmitService()相似。

static void GetRefService(Type type, Boolean cache)
{
var site = _expEngine.CallSiteFactory.CreateCallSite(type, new CallSiteChain());
Func<ServiceProviderEngineScope, object> func;
if (cache)
{
func = _expEngine.RealizedServices.GetOrAdd(type, scope => _expEngine.RuntimeResolver.Resolve(site, scope));
}
else
{
func = scope => _expEngine.RuntimeResolver.Resolve(site, scope);
_expEngine.RealizedServices[type] = func;
}
if (func == null)
{
_logger.Warn("Cache miss");
return;
}
var obj = func(_expEngine.Root);
if (obj == null)
{
throw new NotImplementedException();
}
}

-l|loop

重复执行若干次,每次均记录测试时长

static void TestBar(Action<Type, Boolean> handle, String method, Boolean cache, Type type)
{
_watch.Restart();
handle(type, cache);
_watch.Stop();
_logger.Info("method {0}, cache {1}, target {2}, cost {3}",
method, cache, type.Name, _watch.ElapsedMilliseconds);
} ...
TestBar(handle, method, false, number);
for (int i = 1; i < loop; i++)
{
TestBar(handle, method, cache, number);
}

由于本测试的重点是对比使用反射、表达式树与 emit 的性能与开销,故程序启动后首先遍历 ServiceCollection 对每个组件调用 CallSiteFactory.CreateCallSite(Type serviceType),确保组件的上下文已经被创建和缓存。

启动测试

对以上参数进行组合,得到以下启动方式,测试结果异步写入日志文件供后续解析。

# 启用委托缓存行为,实例化以 IFoo_ 作为命名前缀注入的服务
./LeoninewAxe.Scaffold.DependencyInjection.App.exe -m ref -t foo -c true -n 100 -l 100
./LeoninewAxe.Scaffold.DependencyInjection.App.exe -m ref -t foo -c true -n 1000 -l 100
./LeoninewAxe.Scaffold.DependencyInjection.App.exe -m ref -t foo -c true -n 5000 -l 100
./LeoninewAxe.Scaffold.DependencyInjection.App.exe -m ref -t foo -c true -n 10000 -l 100
./LeoninewAxe.Scaffold.DependencyInjection.App.exe -m exp -t foo -c true -n 100 -l 100
./LeoninewAxe.Scaffold.DependencyInjection.App.exe -m exp -t foo -c true -n 1000 -l 100
./LeoninewAxe.Scaffold.DependencyInjection.App.exe -m exp -t foo -c true -n 5000 -l 100
./LeoninewAxe.Scaffold.DependencyInjection.App.exe -m exp -t foo -c true -n 10000 -l 100
./LeoninewAxe.Scaffold.DependencyInjection.App.exe -m emit -t foo -c true -n 100 -l 100
./LeoninewAxe.Scaffold.DependencyInjection.App.exe -m emit -t foo -c true -n 1000 -l 100
./LeoninewAxe.Scaffold.DependencyInjection.App.exe -m emit -t foo -c true -n 5000 -l 100
./LeoninewAxe.Scaffold.DependencyInjection.App.exe -m emit -t foo -c true -n 10000 -l 100 # 禁用委托缓存行为,实例化以 IFoo_ 作为命名前缀注入的服务
./LeoninewAxe.Scaffold.DependencyInjection.App.exe -m ref -t foo -c false -n 100 -l 50
./LeoninewAxe.Scaffold.DependencyInjection.App.exe -m ref -t foo -c false -n 1000 -l 50
./LeoninewAxe.Scaffold.DependencyInjection.App.exe -m ref -t foo -c false -n 5000 -l 50
./LeoninewAxe.Scaffold.DependencyInjection.App.exe -m ref -t foo -c false -n 10000 -l 50
./LeoninewAxe.Scaffold.DependencyInjection.App.exe -m exp -t foo -c false -n 100 -l 50
./LeoninewAxe.Scaffold.DependencyInjection.App.exe -m exp -t foo -c false -n 1000 -l 50
./LeoninewAxe.Scaffold.DependencyInjection.App.exe -m exp -t foo -c false -n 5000 -l 50
./LeoninewAxe.Scaffold.DependencyInjection.App.exe -m exp -t foo -c false -n 10000 -l 50
./LeoninewAxe.Scaffold.DependencyInjection.App.exe -m emit -t foo -c false -n 100 -l 50
./LeoninewAxe.Scaffold.DependencyInjection.App.exe -m emit -t foo -c false -n 1000 -l 50
./LeoninewAxe.Scaffold.DependencyInjection.App.exe -m emit -t foo -c false -n 5000 -l 50
./LeoninewAxe.Scaffold.DependencyInjection.App.exe -m emit -t foo -c false -n 10000 -l 50 # 启用委托缓存行为,实例化 IBar_100、IBar_1000、IBar_5000、IBar_10000
./LeoninewAxe.Scaffold.DependencyInjection.App.exe -m ref -t bar -c true -n 100 -l 100
./LeoninewAxe.Scaffold.DependencyInjection.App.exe -m ref -t bar -c true -n 1000 -l 100
./LeoninewAxe.Scaffold.DependencyInjection.App.exe -m ref -t bar -c true -n 5000 -l 100
./LeoninewAxe.Scaffold.DependencyInjection.App.exe -m ref -t bar -c true -n 10000 -l 100
./LeoninewAxe.Scaffold.DependencyInjection.App.exe -m exp -t bar -c true -n 100 -l 100
./LeoninewAxe.Scaffold.DependencyInjection.App.exe -m exp -t bar -c true -n 1000 -l 100
./LeoninewAxe.Scaffold.DependencyInjection.App.exe -m exp -t bar -c true -n 5000 -l 100
# ./LeoninewAxe.Scaffold.DependencyInjection.App.exe -m exp -t bar -c true -n 10000 -l 100 # 请求无法完成,抛出 IL 相关异常
./LeoninewAxe.Scaffold.DependencyInjection.App.exe -m emit -t bar -c true -n 100 -l 100
./LeoninewAxe.Scaffold.DependencyInjection.App.exe -m emit -t bar -c true -n 1000 -l 100
./LeoninewAxe.Scaffold.DependencyInjection.App.exe -m emit -t bar -c true -n 5000 -l 100
# ./LeoninewAxe.Scaffold.DependencyInjection.App.exe -m emit -t bar -c true -n 10000 -l 100 # 请求无法完成,抛出 IL 相关异常 # 禁用委托缓存行为,实例化 IBar_100、IBar_1000、IBar_5000、IBar_10000
./LeoninewAxe.Scaffold.DependencyInjection.App.exe -m ref -t bar -c false -n 100 -l 50
./LeoninewAxe.Scaffold.DependencyInjection.App.exe -m ref -t bar -c false -n 1000 -l 50
./LeoninewAxe.Scaffold.DependencyInjection.App.exe -m ref -t bar -c false -n 5000 -l 50
./LeoninewAxe.Scaffold.DependencyInjection.App.exe -m ref -t bar -c false -n 10000 -l 50
./LeoninewAxe.Scaffold.DependencyInjection.App.exe -m exp -t bar -c false -n 100 -l 50
./LeoninewAxe.Scaffold.DependencyInjection.App.exe -m exp -t bar -c false -n 1000 -l 50
./LeoninewAxe.Scaffold.DependencyInjection.App.exe -m exp -t bar -c false -n 5000 -l 50
# ./LeoninewAxe.Scaffold.DependencyInjection.App.exe -m exp -t bar -c false -n 10000 -l 50 # 请求无法完成,抛出 IL 相关异常
./LeoninewAxe.Scaffold.DependencyInjection.App.exe -m emit -t bar -c false -n 100 -l 50
./LeoninewAxe.Scaffold.DependencyInjection.App.exe -m emit -t bar -c false -n 1000 -l 50
./LeoninewAxe.Scaffold.DependencyInjection.App.exe -m emit -t bar -c false -n 5000 -l 50
# ./LeoninewAxe.Scaffold.DependencyInjection.App.exe -m emit -t bar -c false -n 10000 -l 50 # 请求无法完成,抛出 IL 相关异常

值得一提的是,表达式树和 emit 均无法完成 IBar_10000 的实例化,执行中抛出相同异常 "System.InvalidProgramException: The JIT compiler encountered invalid IL code or an internal limitation."

测试结果

使用 LINQPad 编写脚本解析日志,对解析结果使用 Excel 透视作表,得到耗时平均值与标准差。

对于本测试使用到的以 IFoo_ 和 IBar_ 作为命名前缀的接口来说:

  • 1万余接口的注入时间为 10s 左右;
  • 1万余接口的组件的上下文创建时间在 0.6s 左右;
  • 开启委托缓存时,所有实例化方式都能获益;
  • 所有方式实例化 IBar_N 均比实例化 IFoo_0 至 IFoo_N 快非常多;

反射

  • 对缓存不敏感(控制台为 dotnet 3.0 版本);
  • 组件数量增长时,内存使用平稳,完成10000个 IFoo 实例化完成后进程内存增长 49.1MB-35.9MB=13.2MB;

表达式树

  • 随着组件数量增长,对缓存越发敏感;
  • 内存需求增长,完成10000个 IFoo 实例化后进程内存增长75.7MB-35.9MB=39.8MB;
  • 实例化依赖众多的组件时,在缓存下的耗时几乎忽略;

Emit 与表达式差异不大

  • 同表达式树对缓存敏感
  • 内存需求增长,完成10000个 IFoo 实例化后进程内存增长77.7MB-35.9MB=41.8MB;
  • 耗时更不稳定

开销对比

对比1:开启缓存,实例化 IFoo_ 相关组件

对比2:开启缓存,实例化 IBar_ 相关组件

表达树与 emit 方式均无法完成实例化 IBar_10000

对比3:关闭缓存,实例化 IFoo_ 相关组件

对比4:关闭缓存,实例化 IBar_ 相关组件

表达树与 emit 方式均无法完成实例化 IBar_10000

相对于使用反射来说,不开启缓存时表达式树和 emit 既慢内存消耗又高——无论是实例化 IFoo_ 相关组件还是 IBar_ 相关组件,它们均达到更高的内存占用,又频繁地触发 GC,最终 CPU 使用率居高不下。

测试中未使用 GC.SuppressFinalize()处理实例化得到的组件,大量的 IFoo_ 实例回收影响判断,IBar_ 没有这个问题故放出截图。

源代码相关

所有使用到的代码、图片、日志、表格均可以在 leoninew/try-microsoft-di 找到形如:

leoninew 原创,转载请保留出处 www.cnblogs.com/leoninew

Microsoft.Extensions.DependencyInjection 之三:反射可以一战(附源代码)的更多相关文章

  1. Microsoft.Extensions.DependencyInjection 之三:展开测试

    目录 前文回顾 IServiceCallSite CallSiteFactory ServiceProviderEngine CompiledServiceProviderEngine Dynamic ...

  2. 解析 Microsoft.Extensions.DependencyInjection 2.x 版本实现

    项目使用了 Microsoft.Extensions.DependencyInjection 2.x 版本,遇到第2次请求时非常高的内存占用情况,于是作了调查,本文对 3.0 版本仍然适用. 先说结论 ...

  3. 使用诊断工具观察 Microsoft.Extensions.DependencyInjection 2.x 版本的内存占用

    目录 准备工作 大量接口与实现类的生成 elasticsearch+kibana+apm asp.net core 应用 请求与快照 Kibana 上的请求记录 请求耗时的分析 请求内存的分析 第2次 ...

  4. Microsoft.Extensions.DependencyInjection 之二:使用诊断工具观察内存占用

    目录 准备工作 大量接口与实现类的生成 elasticsearch+kibana+apm asp.net core 应用 请求与快照 Kibana 上的请求记录 请求耗时的分析 请求内存的分析 第2次 ...

  5. Microsoft.Extensions.DependencyInjection 之一:解析实现

    [TOC] 前言 项目使用了 Microsoft.Extensions.DependencyInjection 2.x 版本,遇到第2次请求时非常高的内存占用情况,于是作了调查,本文对 3.0 版本仍 ...

  6. DotNetCore跨平台~一起聊聊Microsoft.Extensions.DependencyInjection

    写这篇文章的心情:激动 Microsoft.Extensions.DependencyInjection在github上同样是开源的,它在dotnetcore里被广泛的使用,比起之前的autofac, ...

  7. 使用 Microsoft.Extensions.DependencyInjection 进行依赖注入

    没有 Autofac DryIoc Grace LightInject Lamar Stashbox Unity Ninject 的日子,才是好日子~~~~~~~~~~ Using .NET Core ...

  8. MvvmLight + Microsoft.Extensions.DependencyInjection + WpfApp(.NetCore3.1)

    git clone MvvmLight失败,破网络, 就没有直接修改源码的方式来使用了 Nuget安装MvvmLightLibsStd10 使用GalaSoft.MvvmLight.Command命名 ...

  9. Microsoft.Extensions.DependencyInjection中的Transient依赖注入关系,使用不当会造成内存泄漏

    Microsoft.Extensions.DependencyInjection中(下面简称DI)的Transient依赖注入关系,表示每次DI获取一个全新的注入对象.但是使用Transient依赖注 ...

随机推荐

  1. python openpyxl内存不主动释放 ——关闭Excel工作簿后内存依旧(MemoryError)

    在openpyxl对Excel读写操作过程中,发现内存没有马上释放,如果得多次读取大文件,内存爪机,后续代码就无法运行. 尝试:各种wb.save()或者with open等途径无法解决. 发现:因为 ...

  2. EF Core设置字段默认时间

    ---恢复内容开始--- 在EF的官方文档上只提到了用 Fluent API来设置默认值. 但是我们日常开发中,会把公用字段都写成基类.比如行创建时间 在需要默认时间的字段加上一个特性 [Databa ...

  3. angular之路由

    一.核心问题 路由要解决的核心问题是通过建立url和页面之间的对应的关系,使不同的页面可以通过不用的url来展示. 首先,当用户在浏览器上输入URL后,Angular将获取该URL并将其解析生成一个U ...

  4. 六、springboot 简单优雅是实现短信服务

    前言 上一篇讲了 springboot 集成邮件服务,接下来让我们一起学习下springboot项目中怎么使用短信服务吧. 项目中的短信服务基本上上都会用到,简单的注册验证码,消息通知等等都会用到.所 ...

  5. Kubernetes的RBAC是啥

    RBAC: Role-Based Access Control,基于角色的权限控制,有以下三种角色 Role:角色,它其实是一组规则,定义了一组API对象的操作权限 Subject:被作用者,可以是人 ...

  6. PCIE DMA实现

    基于Spartan-6, Virtex-5/Virtex-6/Virtex-7/7 Series FPGA PCI Express Block Endpoint模块设计PCI Express Endp ...

  7. 纯C语言写的按键驱动,将按键逻辑与按键处理事件分离~

    button drive 杰杰自己写的一个按键驱动,支持单双击.连按.长按:采用回调处理按键事件(自定义消抖时间),使用只需3步,创建按键,按键事件与回调处理函数链接映射,周期检查按键. 源码地址:h ...

  8. 落谷P3941 入阵曲

    题目背景 pdf题面和大样例链接:http://pan.baidu.com/s/1cawM7c 密码:xgxv 丹青千秋酿,一醉解愁肠. 无悔少年枉,只愿壮志狂. 题目描述 小 F 很喜欢数学,但是到 ...

  9. HDU - 1512  Monkey King

    Problem Description Once in a forest, there lived N aggressive monkeys. At the beginning, they each ...

  10. cobalt strike和metasploit结合使用(互相传递shell会话

    攻击机 192.168.5.173 装有msf和cs 受害机 192.168.5.179 win7 0x01 msf 派生 shell 给 Cobalt strike Msfvenom生成木马上线: ...