前文回顾

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. Hyperion: Building the Largest In memory Search Tree

    Introduction 索引在数据管理中起到很重要的作用,很多索引结构都会采用访问速度快而且内存消耗少的trie树,但一般常见的trie树索引结构都强调效率而忽视内存的效率,他们的效率虽然高,但内存 ...

  2. 3DEarth PPT :一款专为GIS系统研发的三维汇报演示系统

    3DEarth PPT(三维地球汇报演示系统)又称 3DGis PPT,是专为GIS系统研发的三维汇报演示系统.对有3DGis系统的客户它可以作为一个组件(dll)嵌入原系统,对没有3DGis系统的客 ...

  3. win7远程连接全屏和窗口模式切换

    最近开发需要win7远程连接,我知道在连接的时候可以设置全屏模式 但是进去之后想要切换就只能通过快捷键了上网查了一下是ctrl+alt+break.网上说的没有错.我查官方文档也是这样.但是我按的时候 ...

  4. http转换为https

    1.下载ssl 证数 百度ssl 证数都有 其中以便宜ssl为例子 注册登陆 选择免费版 可以使用3个月: 申请过程中需要检测该域名是否为本人所有 ,所以邮箱检测或者域名配置 很简单检测就好了: 验证 ...

  5. Python爬虫零基础入门(系列)

    一.前言上一篇演示了如何使用requests模块向网站发送http请求,获取到网页的HTML数据.这篇来演示如何使用BeautifulSoup模块来从HTML文本中提取我们想要的数据. update ...

  6. windows下将jar文件设置为系统服务

    jar文件的执行需要java环境,怎么配置环境相信不用说了 因为不想每次开机都手动启动一次程序,那么我们就需要把它配置成开机自启动的服务,下面就来讲一种方法 首先,我们知道jar文件的执行命令为 ja ...

  7. C++代码注入

    一.C++代码注入原则: 在注入代码中不允许使用API. 在注入代码中不允许使用全局变量. 在注入代码中不允许使用字符串(编译时也被当做全局变量). 在注入代码中不允许使用函数嵌套. 二.注入代码编写 ...

  8. 计算机视觉(二)-opencv之createTrackbar()详解

    摘要: 我学习openCV3看的是<学习openCV3>这本书,很厚的一本,不知道是不是因为自己看的还不是很多,个人觉得里面的有些重要函数讲的不是很详细,比如createTrackbar( ...

  9. vodevs3031 最富有的人

    在你的面前有n堆金子,你只能取走其中的两堆,且总价值为这两堆金子的xor值,你想成为最富有的人,你就要有所选择. 输入描述 Input Description 第一行包含两个正整数n,表示有n堆金子. ...

  10. 从零基础到拿到网易Java实习offer,我做对了哪些事

    作为一个非科班小白,我在读研期间基本是自学Java,从一开始几乎零基础,只有一点点数据结构和Java方面的基础,到最终获得网易游戏的Java实习offer,我大概用了半年左右的时间.本文将会讲到我在这 ...