瞭解 .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 中是需要寫代碼來加載那些程序集的(也是自動)。

如果我們依次加載這些程序集,那麼時間如下:

Milestone Time
Main Method Start 38
Run 739

如果我們使用 Parallel 並行加載這些程序集,那麼時間如下:

Milestone Time
Main Method Start 31
Run 493

可以看到,程序集加載時間有明顯增加。

實際上我們完成的任務是一樣的,但是程序集加載時間顯著增加,這顯然不是我們期望的結果。

在上例中,第一個不到 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/),不得用於商業目的,基於本文修改後的作品務必以相同的許可發佈。如有任何疑問,請與我聯繫

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章