林德熙在 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;
}
}
}
在這段代碼中,Foo
、Bar
、Xxx
、Yyy
、Zzz
、Www
分別在不同的程序集中,我們姑且認爲程序集名稱是 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/),不得用於商業目的,基於本文修改後的作品務必以相同的許可發佈。如有任何疑問,請與我聯繫。