前文回顾

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. 如何解决myeclipse2014突然无法打开的问题

    今天突然发现我的myeclipse2014打开不了,昨晚还好好的,上网找了一下没有找到解决方法,于是新建一个工作区间Workspace Test,再打开File->Switch WorkSpac ...

  2. H5刮刮卡效果

    效果图: 核心就是使用ctx.globalCompositeOperation = 'destination-out'; 全部代码: <!DOCTYPE html> <html> ...

  3. linux系统下使用xampp 丢失mysql root密码 只能远程访问,本地无法连接数据库

    如果在ubuntu 下面 使用xampp这个集成开发环境,却忘记mysql密码. 当出现只能远程访问的,本地无法访问,通常是host改成% 远程访问,本地访问到一个是空壳.这是权限的问题 需要修hos ...

  4. Weex项目快速打包

    安装最新稳定版的Node.js 运行 cnpm install -g weex-toolkit 安装Weex 官方提供的 weex-toolkit 脚手架工具到全局环境中 运行 weex create ...

  5. 【实战】 elasticsearch 写入速度提升的案例分享

    文章首发投稿至InfoQ,[侠梦的开发笔记]公众号,欢迎关注 https://www.infoq.cn/article/t7b52mbzxqkwrrdpVqD2 基本配置 基本配置,5台配置为 24C ...

  6. Linux被中断的系统调用

    慢系统调用,指的是可能永远无法返回,从而使进程永远阻塞的系统调用,比如无客户连接时的accept.无输入时的read都属于慢速系统调用. 在Linux中,当阻塞于某个慢系统调用的进程捕获一个信号,则该 ...

  7. 【MySQL】java.sql.SQLException: The server time zone value

    错误:Could not open JDBC Connection for transaction; nested exception is java.sql.SQLException: The se ...

  8. Cocos Creator 3D 打砖块教程(二) | 子弹发射与摄像机平滑移动

    在线体验链接: http://example.creator-star.cn/block3d/ 前面一篇文章,我们讲了[打砖块]游戏中的3D物体的场景布局.材质资源.物理刚体与碰撞组件,接下来本篇文章 ...

  9. restTemplate getForObject中map传参问题

    在使用restTemplate中getForObject的map传参形式时: 开始时我是这么调用的: RestTemplate rest = new RestTemplate(); Map<St ...

  10. redis缓存+session 实现单点登录

    一.单点登录介绍 单点登录(Single Sign On),简称为 SSO,是目前比较流行的企业业务整合的解决方案之一.SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系 ...