当前位置: 首页 > news >正文

了解 .NET/C# 程序集的加载时机,以便优化程序启动性能

林德熙在 C# 程序集数量对软件启动性能的影响 一文中说到程序集数量对程序启动性能的影响。在那篇文章中,我们得出结论,想同类数量的情况下,程序集的数量越多,程序启动越慢。

额外的,不同的代码编写方式对程序集的加载性能也有影响。本文将介绍 .NET 中程序集的加载时机,了解这个时机能够对启动期间程序集的加载性能带来帮助。


本文内容

    • 程序集加载方式对性能的影响
    • 程序集的加载时机

程序集加载方式对性能的影响

为了直观地说明程序集加载方式对性能的影响,我们先来看一段代码:

using System;
using System.Threading.Tasks;

namespace Walterlv.Demo
{
    public static class Program
    {
        [STAThread]
        private static int Main(string[] args)
        {
            var logger = new StartupLogger();
            var startupManagerTask = Task.Run(() =>
            {
                var startup = new StartupManager(logger).ConfigAssemblies(
                    new Foo(),
                    new Bar(),
                    new Xxx(),
                    new Yyy(),
                    new Zzz(),
                    new Www());
                startup.Run();
                return startup;
            });

            var app = new App(startupManagerTask);
            app.InitializeComponent();
            app.Run();

            return 0;
        }
    }
}

在这段代码中,FooBarXxxYyyZzzWww 分别在不同的程序集中,我们姑且认为程序集名称是 FooAssembly、BarAssembly、XxxAssembly、YyyAssembly、ZzzAssembly、WwwAssembly。

现在,我们统计 Main 函数开始第一句话到 Run 函数开始执行时的时间:

| 统计 | Milestone | Time |
| 第一次 | -------------------------------- | -------: |
| 第一次 | Main Method Start | 107 |
| 第一次 | Run | 344 |
| 第二次 | Main Method Start | 106 |
| 第二次 | Run | 276 |
| 第三次 | Main Method Start | 89 |
| 第三次 | Run | 224 |

在三次统计中,我们可以看到三次平均时长 180 ms。如果观察没一句执行时的 Module,可以看到 Main 函数开始时,这些程序集都未加载,而 Run 函数执行时,这些程序集都已加载。

事实上,如果你把断点放在 Task.Run 中 lambda 表达式的第一个括号处,你会发现那一句时这些程序集就已经加载了,不用等到后面代码的执行。

作为对比,我需要放上没有程序集加载时候的数据(具体来说,就是去掉所有 new 那些类的代码):

| 统计 | Milestone | Time |
| 第一次 | -------------------------------- | -------: |
| 第一次 | Main Method Start | 43 |
| 第一次 | Run | 75 |
| 第二次 | Main Method Start | 27 |
| 第二次 | Run | 35 |
| 第三次 | Main Method Start | 28 |
| 第三次 | Run | 40 |

这可以证明,以上时间大部分来源于程序集的加载,而不是其他什么代码。

现在,我们稍稍修改一下程序集,让 new Foo() 改为使用 lambda 表达式来创建:

    using System;
    using System.Threading.Tasks;
    
    namespace Walterlv.Demo
    {
        public static class Program
        {
            [STAThread]
            private static int Main(string[] args)
            {
                var logger = new StartupLogger();
                var startupManagerTask = Task.Run(() =>
                {
                    var startup = new StartupManager(logger).ConfigAssemblies(
--                      new Foo(),
--                      new Bar(),
--                      new Xxx(),
--                      new Yyy(),
--                      new Zzz(),
--                      new Www());
++                      () => new Foo(),
++                      () => new Bar(),
++                      () => new Xxx(),
++                      () => new Yyy(),
++                      () => new Zzz(),
++                      () => new Www());
                    startup.Run();
                    return startup;
                });
    
                var app = new App(startupManagerTask);
                app.InitializeComponent();
                app.Run();
    
                return 0;
            }
        }
    }

这时,直到 Run 函数执行时,那些程序集都还没有加载。由于我在 Run 函数中真正使用到了那些对象,所以其实 Run 中是需要写代码来加载那些程序集的(也是自动)。

如果我们依次加载这些程序集,那么时间如下:

MilestoneTime
Main Method Start38
Run739

如果我们使用 Parallel 并行加载这些程序集,那么时间如下:

MilestoneTime
Main Method Start31
Run493

可以看到,程序集加载时间有明显增加。

实际上我们完成的任务是一样的,但是程序集加载时间显著增加,这显然不是我们期望的结果。

在上例中,第一个不到 200 ms 的加载时间,来源于我们直接写下了 new 不同程序集中的类型。后面长一些的时间,则因为我们的 Main 函数中没有直接构造类型,而是写成了 lambda 表达式。来源于在 Run 中调用那些 lambda 表达式从而间接加载了类型。

为了更直观,我把 Run 方法中的关键代码贴出来:

// assemblies 是直接 new 出来的参数传进来的。
_assembliesToBeManaged.AddRange(assemblies);
// assemblies 是写的 lambda 表达式参数传进来的。
_assembliesToBeManaged.AddRange(assemblies.Select(x => x()));

上面的版本,这些程序集的加载时间是 180 ms,而下面的版本,则达到惊人的 701 ms!

程序集的加载时机

于是我们可以了解到程序集的加载时机。

  • 在一个方法被 JIT 加载的时候,里面用到的类型所在的程序集就会被加载到应用程序域中。当加载完后,此方法才被执行。
  • 加载程序集时,只会加载方法中会直接使用到的类型,如果是 lambda 内的类型,则会在此 lambda 被调用的时候才会执行(其实这本质上和方法被调用之前的加载是一个时机)。

并且,我们能够得出性能优化建议:

  • 如果可行,最好让 CLR 自动管理程序集的加载,而且一次性能加载所有程序集的话就一次性加载,而不要尝试自己去分开加载这些程序集,那会使得能够并行的加载程序集的时间变得串行,浪费启动性能。

我的博客会首发于 https://blog.walterlv.com/,而 CSDN 会从其中精选发布,但是一旦发布了就很少更新。

如果在博客看到有任何不懂的内容,欢迎交流。我搭建了 dotnet 职业技术学院 欢迎大家加入。

知识共享许可协议

本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。欢迎转载、使用、重新发布,但务必保留文章署名吕毅(包含链接:https://walterlv.blog.csdn.net/),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请与我联系。

相关文章:

  • git 如何更可靠地解决冲突?
  • .NET 编写一个可以异步等待循环中任何一个部分的 Awaiter
  • 文件被占用?系统自带的“资源监视器(resmon)”也能帮你找到占用它的真凶
  • Windows 系统文件资源管理器的命令行参数(如何降权打开程序,如何选择文件)
  • 为 .NET 各种开发工具设置网络代理,提升在大陆的网络性能
  • 如何在旧版本的 .NET Core / Framework 中使用 C# 8 的异步流(IAsyncDisposable / IAsyncEnumerable / IAsyncEnumerator)
  • .NET/C# 解压 Zip 文件时出现异常:System.IO.InvalidDataException: 找不到中央目录结尾记录。
  • 为什么实现 .NET 的 ICollection 集合时需要实现 SyncRoot 属性?如何正确实现这个属性?
  • 为什么不应该公开用来同步的加锁对象?为什么不应该 lock(this)/lock(string) 或者 lock 任何非私有对象?
  • WPF 中如何创建忽略 DPI 属性的图片
  • .NET 中选择合适的文件打开模式(CreateNew, Create, Open, OpenOrCreate, Truncate, Append)
  • Win32 方法 CreateFile 中选择合适的文件打开模式(CREATE_NEW, CREATE_ALWAYS, OPEN_EXISTING, OPEN_ALWAYS, TRUNCATE_EXI
  • 使用 Kestrel 为你的 ASP.NET Core 服务添加 https 支持
  • 三种方法为 ASP.NET Core 对外服务添加 https 支持(kestrel / frp / nginx)
  • 为 ASP.NET Core 程序制作 URL 的 301/302 跳转
  • 【108天】Java——《Head First Java》笔记(第1-4章)
  • 30天自制操作系统-2
  • Android交互
  •  D - 粉碎叛乱F - 其他起义
  • gulp 教程
  • HTTP传输编码增加了传输量,只为解决这一个问题 | 实用 HTTP
  • iOS编译提示和导航提示
  • Spring Security中异常上抛机制及对于转型处理的一些感悟
  • springboot_database项目介绍
  • 给第三方使用接口的 URL 签名实现
  • 湖南卫视:中国白领因网络偷菜成当代最寂寞的人?
  • 漫谈开发设计中的一些“原则”及“设计哲学”
  • 如何胜任知名企业的商业数据分析师?
  • 算法-插入排序
  • 腾讯大梁:DevOps最后一棒,有效构建海量运营的持续反馈能力
  • 用element的upload组件实现多图片上传和压缩
  • 在Mac OS X上安装 Ruby运行环境
  • 自动记录MySQL慢查询快照脚本
  • ​猴子吃桃问题:每天都吃了前一天剩下的一半多一个。
  • # 执行时间 统计mysql_一文说尽 MySQL 优化原理
  • (04)odoo视图操作
  • (1)安装hadoop之虚拟机准备(配置IP与主机名)
  • (16)Reactor的测试——响应式Spring的道法术器
  • (附源码)ssm航空客运订票系统 毕业设计 141612
  • (附源码)计算机毕业设计ssm基于B_S的汽车售后服务管理系统
  • (蓝桥杯每日一题)平方末尾及补充(常用的字符串函数功能)
  • (理论篇)httpmoudle和httphandler一览
  • (六)Hibernate的二级缓存
  • (三)Pytorch快速搭建卷积神经网络模型实现手写数字识别(代码+详细注解)
  • (算法)Game
  • (幽默漫画)有个程序员老公,是怎样的体验?
  • (转载)利用webkit抓取动态网页和链接
  • (总结)Linux下的暴力密码在线破解工具Hydra详解
  • *Django中的Ajax 纯js的书写样式1
  • .bat批处理(十一):替换字符串中包含百分号%的子串
  • .form文件_SSM框架文件上传篇
  • .Net - 类的介绍
  • .NET CLR Hosting 简介
  • .NET core 自定义过滤器 Filter 实现webapi RestFul 统一接口数据返回格式
  • .net MVC中使用angularJs刷新页面数据列表