Microsoft.Extensions.DependencyInjection 之三:反射可以一战(附源代码)
前文回顾
Microsoft.Extensions.DependencyInjection 之一:解析实现 提到了 Microsoft.Extensions.DependencyInjection 包含以下核心组件。
IServiceCallSite
组件实例化上下文,包含许多实现,仅列举其中的ConstantCallSite
,CreateInstanceCallSite
,ConstructorCallSite
如下。
ConstructorCallSite
既是IServiceCallSite
的实现类,又复合引用IServiceCallSite
,以表达自身参数的构建形式。
组件生命周期也使用IServiceCallSite
表达,它们既从IServiceCallSite
继承,也引用IServiceCallSite
。
CallSiteFactory
当组件需要被实例化时,CallSiteFactory
从维护的ServiceDescriptor
查找注入方式,对类型注入的组件使用反射解析其构造函数,并递归解析其参数,最后缓存得到的IServiceCallSite
实例。
ServiceProviderEngine
ServiceProviderEngine
是抽象类,内部依赖CallSiteRuntimeResolver
完成基于反射的组件实例化,并缓存了组件实例化的委托。
CompiledServiceProviderEngine
CompiledServiceProviderEngine
从ServiceProviderEngine
继承,内部依赖ExpressionResolverBuilder
完成基于表达式树的组件实例化的委托。
DynamicServiceProviderEngine
DynamicServiceProviderEngine
从CompiledServiceProviderEngine
继承,它创建的委托比较特殊:
- 该委托第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 后,提取到ServiceProviderEngine
、CompiledServiceProviderEngine
和ILEmitServiceProviderEngine
等核心实现,再编写控制台程序,对依赖注入中反射、表达式树、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 之三:反射可以一战(附源代码)的更多相关文章
- Microsoft.Extensions.DependencyInjection 之三:展开测试
目录 前文回顾 IServiceCallSite CallSiteFactory ServiceProviderEngine CompiledServiceProviderEngine Dynamic ...
- 解析 Microsoft.Extensions.DependencyInjection 2.x 版本实现
项目使用了 Microsoft.Extensions.DependencyInjection 2.x 版本,遇到第2次请求时非常高的内存占用情况,于是作了调查,本文对 3.0 版本仍然适用. 先说结论 ...
- 使用诊断工具观察 Microsoft.Extensions.DependencyInjection 2.x 版本的内存占用
目录 准备工作 大量接口与实现类的生成 elasticsearch+kibana+apm asp.net core 应用 请求与快照 Kibana 上的请求记录 请求耗时的分析 请求内存的分析 第2次 ...
- Microsoft.Extensions.DependencyInjection 之二:使用诊断工具观察内存占用
目录 准备工作 大量接口与实现类的生成 elasticsearch+kibana+apm asp.net core 应用 请求与快照 Kibana 上的请求记录 请求耗时的分析 请求内存的分析 第2次 ...
- Microsoft.Extensions.DependencyInjection 之一:解析实现
[TOC] 前言 项目使用了 Microsoft.Extensions.DependencyInjection 2.x 版本,遇到第2次请求时非常高的内存占用情况,于是作了调查,本文对 3.0 版本仍 ...
- DotNetCore跨平台~一起聊聊Microsoft.Extensions.DependencyInjection
写这篇文章的心情:激动 Microsoft.Extensions.DependencyInjection在github上同样是开源的,它在dotnetcore里被广泛的使用,比起之前的autofac, ...
- 使用 Microsoft.Extensions.DependencyInjection 进行依赖注入
没有 Autofac DryIoc Grace LightInject Lamar Stashbox Unity Ninject 的日子,才是好日子~~~~~~~~~~ Using .NET Core ...
- MvvmLight + Microsoft.Extensions.DependencyInjection + WpfApp(.NetCore3.1)
git clone MvvmLight失败,破网络, 就没有直接修改源码的方式来使用了 Nuget安装MvvmLightLibsStd10 使用GalaSoft.MvvmLight.Command命名 ...
- Microsoft.Extensions.DependencyInjection中的Transient依赖注入关系,使用不当会造成内存泄漏
Microsoft.Extensions.DependencyInjection中(下面简称DI)的Transient依赖注入关系,表示每次DI获取一个全新的注入对象.但是使用Transient依赖注 ...
随机推荐
- Hyperion: Building the Largest In memory Search Tree
Introduction 索引在数据管理中起到很重要的作用,很多索引结构都会采用访问速度快而且内存消耗少的trie树,但一般常见的trie树索引结构都强调效率而忽视内存的效率,他们的效率虽然高,但内存 ...
- 3DEarth PPT :一款专为GIS系统研发的三维汇报演示系统
3DEarth PPT(三维地球汇报演示系统)又称 3DGis PPT,是专为GIS系统研发的三维汇报演示系统.对有3DGis系统的客户它可以作为一个组件(dll)嵌入原系统,对没有3DGis系统的客 ...
- win7远程连接全屏和窗口模式切换
最近开发需要win7远程连接,我知道在连接的时候可以设置全屏模式 但是进去之后想要切换就只能通过快捷键了上网查了一下是ctrl+alt+break.网上说的没有错.我查官方文档也是这样.但是我按的时候 ...
- http转换为https
1.下载ssl 证数 百度ssl 证数都有 其中以便宜ssl为例子 注册登陆 选择免费版 可以使用3个月: 申请过程中需要检测该域名是否为本人所有 ,所以邮箱检测或者域名配置 很简单检测就好了: 验证 ...
- Python爬虫零基础入门(系列)
一.前言上一篇演示了如何使用requests模块向网站发送http请求,获取到网页的HTML数据.这篇来演示如何使用BeautifulSoup模块来从HTML文本中提取我们想要的数据. update ...
- windows下将jar文件设置为系统服务
jar文件的执行需要java环境,怎么配置环境相信不用说了 因为不想每次开机都手动启动一次程序,那么我们就需要把它配置成开机自启动的服务,下面就来讲一种方法 首先,我们知道jar文件的执行命令为 ja ...
- C++代码注入
一.C++代码注入原则: 在注入代码中不允许使用API. 在注入代码中不允许使用全局变量. 在注入代码中不允许使用字符串(编译时也被当做全局变量). 在注入代码中不允许使用函数嵌套. 二.注入代码编写 ...
- 计算机视觉(二)-opencv之createTrackbar()详解
摘要: 我学习openCV3看的是<学习openCV3>这本书,很厚的一本,不知道是不是因为自己看的还不是很多,个人觉得里面的有些重要函数讲的不是很详细,比如createTrackbar( ...
- vodevs3031 最富有的人
在你的面前有n堆金子,你只能取走其中的两堆,且总价值为这两堆金子的xor值,你想成为最富有的人,你就要有所选择. 输入描述 Input Description 第一行包含两个正整数n,表示有n堆金子. ...
- 从零基础到拿到网易Java实习offer,我做对了哪些事
作为一个非科班小白,我在读研期间基本是自学Java,从一开始几乎零基础,只有一点点数据结构和Java方面的基础,到最终获得网易游戏的Java实习offer,我大概用了半年左右的时间.本文将会讲到我在这 ...