不安裝運行時運行 .NET 程序 - NativeAOT

大家好,先祝大家國慶快樂。不過大家看到這篇文章的時候估計已經過完國慶了 😃。
上一篇我們寫了如何通過 SelfContained 模式發佈程序(不安裝運行時運行.NET程序)達到不需要在目標機器上安裝 runtime 就可以運行 .NET 程序的目標。其實除了標準的 self-contained 微軟還給我們帶來了 Native AOT 發佈模式。是的你沒看錯,通過該技術我們的 .NET 程序會直接編譯爲 Native 代碼而不再是 IL ,程序運行的時候直接就是機器碼,不再需要 JIT 編譯。通過 AOT 技術,我們的程序啓動會變的非常快並且使用更少的內存,並且運行的時候不需要在機器上安裝任何運行時。
前階段 .NET7 發佈了第一個 RC 版本,標誌着正式版的 AOT 馬上會隨 .NET7 發佈而到來。所以趁着國慶趕緊體驗一把。

環境與工具

現階段 .NET7 還在RC,所以我們選擇安裝 SDK 7.0.100-rc.1.22431.12 ,操作系統是 WIN10 64位,開發工具是 VS2022 17.4.0 Preview 2.1 。正式版的 VS2022 是沒辦法選擇目標框架 .NET7 的,但是其實可以手動改 csproj 文件,所以 VS2022 Preview 不是必須的。

Console App

我們新建一個控制檯程序,目標框架選擇 NET7 (如果使用正式版的 VS2022 沒有辦法選擇 net7 ,可以直接編輯 csproj 文件),右鍵項目選擇“編輯項目文件”,在 PropertyGroup 節點下添加 PublishAot :

<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net7.0</TargetFramework>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
		<!--aot發佈-->
        <PublishAot>true</PublishAot>
		
    </PropertyGroup>

</Project>

修改 main 方法:

Console.WriteLine("Hello, AOT!");

Console.Read();

使用 dotnet 命令進行發佈:

dotnet publish -r win-x64 -c Release


AOT 發佈相比正常發佈會慢一點,等待發布成功後,我們可以到以下目錄查看 bin\Release\net7.0\win-x64\publish :

我們可以看到生成的 exe 文件只有 3.48MB ,相比普通單文件發佈加裁剪過後的程序小了不少。

我們把這個 exe 程序複製到一臺沒有安裝 .net 環境的服務器上,順利運行起來了。

ASP.NET CORE

上面我們測試了一下控制檯程序的 AOT 發佈,相對比較簡單沒有什麼問題。下面讓我們試試應用範圍最爲廣泛的 ASP.NET CORE 項目 AOT 發佈行不行。
新建一個 ASP.NET CORE WebApi 項目,目標框架選擇 NET7 。同樣的操作編輯 csproj 文件,添加 PublishAot 屬性:

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net7.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>

    <PublishAot>true</PublishAot>
  </PropertyGroup>

</Project>

同樣使用 dotnet cli 命令進行發佈:

dotnet publish -r win-x64 -c Release

不同於上面控制檯項目的發佈,ASP.NET CORE 項目的 AOT 發佈會出現很多警告信息,暫且忽略。

等到發佈完成後,我們看到生成了一個 27MB 大小的 exe 文件。雙擊運行起來,不得不提一句,這個啓動速度真的是肉眼可見的快,雙擊之後瞬間就啓動了。這個就是 AOT 發佈最大的優勢了。

訪問一下默認生成的那個 Action 方法:http://localhost:5000/WeatherForecast/ 成功的輸出了天氣信息。

序列化的問題

以上通過簡單的測試,ASP.NET CORE WebApi 項目順利的跑起來了, 當然他只是一個簡單的示例項目,我們生產的項目相比這些要複雜多了。經過更深入的測試,發現現階段 ASP.NET CORE 進行 AOT 發佈後有一個比較麻煩的問題,那就是 JSON 序列化。
以下代碼是默認生成的 WeatherForecastController 的 GET 方法,這個方法是個標準的同步方法,進行 AOT 發佈後序列化沒有任何問題。

   [HttpGet]
        public WeatherForecast[] Get()
        {
            return Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Date = DateTime.Now.AddDays(index),
                TemperatureC = Random.Shared.Next(-20, 55),
                Summary = Summaries[Random.Shared.Next(Summaries.Length)]
            })
            .ToArray();
        }

但是如果把代碼改成異步,或者說的更直白一點的話,返回值是 Task<T> 類型就會出現問題。比如把上面的代碼使用 Task.FromResult 改造一下,使返回值變成 Task<WeatherForecast[]>

       [HttpGet]
        public async Task<WeatherForecast[]> Get()
        {
            var arr = Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Date = DateTime.Now.AddDays(index),
                TemperatureC = Random.Shared.Next(-20, 55),
                Summary = Summaries[Random.Shared.Next(Summaries.Length)]
            })
            .ToArray();

            var result = await Task.FromResult(arr);

            return result;
        }

改造的程序進行 AOT 發佈後運行,訪問對應的接口程序不會有任何報錯,但是返回值是個空對象的json:

{}

嘗試修復該問題,並沒有特別的好辦法,目前能夠勉強使用的辦法是使用 System.Text.Json source generator 模式進行序列化:
首先編寫一個 WeatherForecastContext 類繼承 JsonSerializerContext,並且標記爲 partial。爲啥要標記爲 partial ?因爲類的另外部分是 source generator 自動生成的。

   [JsonSerializable(typeof(Task<WeatherForecast[]>))]
    internal partial class WeatherForecastContext : JsonSerializerContext
    {
    }

第二步,在配置 services 的時候順便把 WeatherForecastContext 配置進去。

builder.Services.AddControllers()
    .AddJsonOptions(options => options.JsonSerializerOptions.AddContext<WeatherForecastContext>()); 

通過以上操作,再次 AOT 發佈後運行程序,訪問接口,數據是能正確的返回了。但是有一點小瑕疵是Task對象自身的屬性也被序列化出來了。

{
    "result": [
        {
            "date": "2022-10-08T19:14:26.1801524+08:00",
            "temperatureC": 6,
            "temperatureF": 42,
            "summary": "Warm"
        },
        {
            "date": "2022-10-09T19:14:26.1816645+08:00",
            "temperatureC": -9,
            "temperatureF": 16,
            "summary": "Bracing"
        },
        {
            "date": "2022-10-10T19:14:26.1816648+08:00",
            "temperatureC": -1,
            "temperatureF": 31,
            "summary": "Sweltering"
        },
        {
            "date": "2022-10-11T19:14:26.181665+08:00",
            "temperatureC": -17,
            "temperatureF": 2,
            "summary": "Balmy"
        },
        {
            "date": "2022-10-12T19:14:26.1816651+08:00",
            "temperatureC": -16,
            "temperatureF": 4,
            "summary": "Freezing"
        }
    ],
    "asyncState": null,
    "creationOptions": 0,
    "exception": null,
    "id": 1,
    "isCanceled": false,
    "isCompleted": true,
    "isCompletedSuccessfully": true,
    "isFaulted": false,
    "status": 5
}

桌面程序

以上對控制檯程序,web 程序進行了測試,接下來順便對桌面 GUI 程序測試一下吧。

很遺憾,不管是 WINFROM 還是 WPF 程序,進行 AOT 發佈的時候直接都會報錯,提示不支持。

一些其他限制

AOT 發佈的程序會有一些限制,我們編寫的時候需要注意:

  1. No dynamic loading (for example, Assembly.LoadFile)
  2. No runtime code generation (for example, System.Reflection.Emit)
  3. No C++/CLI
  4. No built-in COM (only applies to Windows)
  5. Requires trimming, which has limitations
  6. Implies compilation into a single file, which has known incompatibilities
  7. Apps include required runtime libraries (just like self-contained apps, increasing their size, as compared to framework-dependent apps)

以上是直接複製的英文文檔(原文地址在文末),因爲英文不是很好,不進行翻譯了,怕誤導大家。主要需要注意的就是 1,2 兩點 ,關於動態加載類庫跟動態生成代碼的問題。我想序列化的問題大概也就是出在這裏,因爲傳統的序列化需要大量的使用動態生成代碼技術。

總結

通過以上我們對 .NET 上最常用的幾種程序進行了 Native AOT 發佈的測試。總體來說控制檯跟ASP.NET CORE 項目能用,WINFROM 跟 WPF 不能用。比較遺憾的有兩個點:

  1. ASP.NET COER 在序列化方面貌似還有點小問題。不知道是不是我環境的問題,如果有知道的大神請指點指點
  2. 不支持桌面 GUI 程序。其實從個人的經驗來說,桌面端可能對啓動速度更加敏感一點,因爲c/s程序經常性的打開關閉、打開關閉,如果啓動慢用戶是很容易察覺的。如果桌面程序能支持 AOT ,那麼能大大改進現在 .NET 程序的啓動速度,這對用戶體驗的提升是非常大的。服務端的話本身啓動一次後就長期運行,用戶不會時時刻刻感受到啓動速度帶來的影響。另外現在 .NET 程序啓動本身就不慢,況且還有 R2R 可以選,正常在100-200ms之間的啓動速度已經對用戶體驗影響不大了。所以 AOT 之後的啓動速度的優勢不是很大。

另外來說說性能,有同學可能覺得 Native AOT 之後性能會有很大的提升,畢竟大家都迷信 Native 速度快嘛。但是經過大佬們的測試事實上 AOT 之後跟沒有 AOT 的代碼性能基本在伯仲之間,有些地方甚至不如非 Native 的代碼。爲什麼?因爲非 Native 代碼可以進行運行時 JIT 啊,可以在運行時分析代碼對熱點代碼進行二次 JIT 來提升性能,而 Native AOT 之後的代碼做不到這點。

參考

Native AOT Deployment
Try the new System.Text.Json source generator
AOT和單文件發佈對程序性能的影響

關注我的公衆號一起玩轉技術

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