.NET Core 3.0

待望の.NET Core 3.0がついにリリースされました。

LTSはもう少し待たないといけませんが、この3.0も待ち望んでいました。

 

たくさんの改善点がありますが、今回紹介したいのは、publishの新しいオプションです。

今まで

今まで、.NET Coreアプリは、基本的にはdllで出力され、self-containedなものを作ってもexeはただのランチャーでした。またこのself-containedがデカい。しかもファイル数が多い。

まぁ、デプロイ時に日付比較して新しいものだけを入れる事ができるので、更新は効率的となっています。ただ、現実問題としてデプロイはフルパッケージで出しますよね。実際にはたくさんのファイルを比較して(なぜかtouchされてた!みたいなの含め)比較ミスがあるよりも、フルパッケージで更新する方が無難です。

 

このファイル数の問題もそうですが、起動時にアセンブリを読み込むこと自体が少し遅かったりで、.NET Coreアプリは何十回とサクッと起動してサクッと終わる類いのアプリに向いていないと言わざるを得ませんでした。

.NET Core 3.0から

今回の3.0から、publishにオプションが追加されました。

publishの方法自体が、dllを出力する形でなくなり、基本的にexecutableが作成されるように変更されたりもしましたが、今回はself-containedなものを対象に、publishオプションを見ていきたいと思います。

新しいオプション

PublishTrimmed

ILを調査して不要なものを削除するIL Linkerをかけるオプションです。

.netでは、アセンブリ(DLL)を動的にロードしたりすることを想定し、今までは基本的にアプリケーションから参照したアセンブリは、そのままの状態で出力されていました。

これは、.NET Frameworkでも同じです。

 

しかし、あるアプリケーションから見たときに、本当に使用するコードはその参照アセンブリの一部だけ、なんて言うことも多くありません。

Console.WriteLineするだけのHello WorldがConsoleクラスの全機能を使うかと言われればもちろん違いますし、あのビッグなmscorelibだってそうです。

そこで、本当に使用するメソッドやプロパティだけを残し、不必要なものを削除するオプションになります。

また、参照しているものからさらに参照されているアセンブリのうち、不要なものはファイルごと削除されます。

これは、.csprojに下記の記載をすることにより有効になります。

   <PublishTrimmed>true</PublishTrimmed>

実際にどれぐらい効くかはまた後述しますが、結構な容量の節約になります。

PublishSingleFile

前述の通り、.NET Coreでは大量の参照アセンブリを出力し、これは、PublishTrimmedをかけても同じ(数は減ります)です。

そこで、このファイルを一つの実行可能形式にしてしまうオプションが追加されました。

.csprojに下記のように記述するか、

   <RuntimeIdentifier>win10-x64</RuntimeIdentifier>
   <PublishSingleFile>true</PublishSingleFile>

ビルド時にコマンドラインオプションとして設定します。

dotnet publish -r win10-x64 -c Release --self-contained true /p:PublishSingleFile=true

実行ファイルを作る都合上、必ずRuntimeIdentifierで対象を設定する必要があります。

.NET Coreだし、クロス前提! という方は、コマンドラインオプションで指定するようにしておいても良いかもしれません。

 

ただ、現在、このオプションは「すべてを圧縮して一つのファイルにする」という発想で、初回起動時にテンポラリにすべて展開して起動するという荒技を行っています。

ReadyToRun

.NET Core 3.0からTiered Compilationが標準で有効になりました。

これは、初回起動時にJITが本気を出して最適化していると結果的に起動が遅い件を解消するもので、最初は手加減したゆるいJITをかけて起動し、その後気合いを入れて本格的な最適化を行うJITをかけ直していくものです。

ただ、それで万事解決かというと、そうではなく依然として起動に少し時間がかかることには変わりません。

じゃあ、もういっそ最初からJITじゃなくてNativeなコードにコンパイルしちゃおうぜ、というオプションが、ReadyToRunです。

そしたらもう.netのアセンブリいらないんじゃないの? と思いたくなりますが、実際にはジェネリクスなんかだったり、動的なコード生成なんかを.netは行えるので、アセンブリは必要となります。

.NET 5では、完全オールNativeにしちゃいたいなぁ、みたいなことは言ってますけどね。

 

で、このReadyToRunは、.csprojに下記のように記載します。

  <RuntimeIdentifier>win10-x64</RuntimeIdentifier>
  <PublishReadyToRun>true</PublishReadyToRun>

RuntimeIdentifierはコンパイルオプションで指定してもかまいませんが、Nativeコードを生成する以上必要になります。

このオプションを使うと、ほんの少し標準ビルドよりファイルサイズが増えます。

 

オプションの使用

これらのオプションは、排他ではなく、同時に使用することが出来ます。

PublishTrimmedでサイズが小さくなり、ReadyToRunでサイズが少し大きくなり、PublishSingleFileでファイル数が減るもののサイズが増えることになります。

 

じゃあ、それぞれどうなるのよ? ということで、下記のくだらないソースを実際にそれぞれのオプションでビルドして、実行してみました。

なお、テスト環境はMem 16GでSSDの、i3 7th GenのWindowsノートPCです。

using System;

namespace core3publishtest
{
    class Program{
        static int Tarai2(int x,int y,int z)
        {
            if(x<=y) return y;
            return Tarai(Tarai(x-1,y,z),Tarai(y-1,z,x),Tarai(z-1,x,y));
        }
        static int Tarai(int x,int y,int z)
        {
            if(x<=y) return y;
            return Tarai2(Tarai2(x-1,y,z),Tarai2(y-1,z,x),Tarai2(z-1,x,y));
        }
        static int Fact(int n)
        {
            if(n==1) return 1;
            return n*Fact(n-1);
        }
        static long DumbFunction()
        {
            var ret = 1L;
            var i=0;
            while(ret<1000000000L)
            {
                i++;
                if(i%5==0) ret++;
                if(i%3==0) ret+=i;
                if(i%25==0) ret+=7;
                if((i+ret) % 13==0) ret+=2;
            }
            return ret;
        }
        static string GetHelloWorld()
        {
            return $"Hello World And Factorial(25) is {Fact(25)} And Tarai(12,6,0) is {Tarai(12,6,0)} And Dumb {DumbFunction()}";
        }
        static void HelloWorld()
        {
            var helloworld = GetHelloWorld();
            Console.WriteLine(helloworld);
        }
        static void Main(string[] args)
        {
            HelloWorld();
        }
    }
}

各計測結果

括弧内の数字は、それぞれオプションなし比、PublishSingleFileオプションなし比です。

単位はミリ秒、個、バイトです。

ビルド時間

ビルド時間オプションなしTrimmedReadyToRunTrimmed+ReadyToRun
オプションなし30685147
(167.7%)
3509
(114.3%)
5787
(188.6%)
PublishSingleFile3889
(-)
(126.7%)
4962
(127.5%)
(161.7%)
3635
(93.4%)
(118.4%)
5634
(144.8%)
(183.6%)

やはりオプションをつけるとビルド時間は長くなりますね。

Trimmedを作成するのがやっぱり大変そう。

ファイル数

ファイル数オプションなしTrimmedReadyToRunTrimmed+ReadyToRun
オプションなし2256322563
PublishSingleFile2222

PublishTrimmedするだけで相当ファイル減ります。これはすごい。

というか元が多すぎる。

PublishTrimmedを行うと、ファイルの中身も変わります。

そのアプリケーション専用のDLLになっちゃう。

 

しかし、さすがPublishSingleFileですね。

1exeファイル+1pdbファイル。圧倒的。

しかし、これが初回起動時間に効いてしまいます。

総ファイルサイズ

総ファイルサイズオプションなしTrimmedReadyToRunTrimmed+ReadyToRun
オプションなし69,003,37526,480,099
(38.3%)
69,006,443
(100%)
26,678,755
(38.6%)
PublishSingleFile69,013,886
(-)
(100%)
26,483,003
(38.3%)
(38.3%)
69,016,954
(100%)
(100%)
26,681,650
(38.6%)
(38.6%)

相変わらずデカいですが、オプションなしでも小さくなっている気がします。

思ったよりオプションなし→PublishSingleFileで増えないし、オプションなし→ReadyToRunでも増えない。

ただ、これはプロジェクトによりますね。

ここでもPublishTrimmedが相当効いていることがわかります。

PublishTrimmed+ReadyToRunの重ね掛けでも全然大丈夫です。

初回実行時間

初回実行時間オプションなしTrimmedReadyToRunTrimmed+ReadyToRun
オプションなし465414
(89%)
461
(99.1%)
371
(79.7%)
PublishSingleFile6280
(-)
(1350.5%)
917
(14.6%)
(197.2%)
4307
(68.5%)
(926.2%)
894
(14.2%)
(192.2%)

ReadyToRunで早くなるのは、割と僅差。

もっと大きいプロジェクトでテストする必要があるかも。

それよりもTrimmedで早くなるのが凄い。

読み込むアセンブリが減るからでしょうか。

そしてPublishSingleFile。

動きからして仕方ないけれども、結構ひどい。

発想が荒いので。仕方ないけど、結構がっかり。

2回目実行時間

2回目実行時間オプションなしTrimmedReadyToRunTrimmed+ReadyToRun
オプションなし157144
(91.7%)
161
(102.5%)
128
(81.5%)
PublishSingleFile150
(-)
(95.5%)
138
(92%)
(87.8%)
162
(108%)
(103.1%)
134
(89.3%)
(85.3%)

2回目になると、PublishSingleFileはどこかに展開結果を持っているのか、早くなります。

もとより早くなるのは謎。

そしてReadyToRunに関しては悪化。

2回目以降実行時間平均

2回目以降実行時間平均オプションなしTrimmedReadyToRunTrimmed+ReadyToRun
オプションなし160.75143.5
(89.3%)
166.5
(103.7%)
147.75
(91.8%)
PublishSingleFile160
(-)
(100%)
135.75
(84.3%)
(84.3%)
178.5
(111.2%)
(111.2%)
141
(88.1%)
(88.1%)

5回実行して、最初の結果以外の平均をとったところ、上記のようになりました。

まあまあよくなってる感はありますね。

ReadyToRunはなぜ悪化するのか。。

連続して実行しているので、キャッシュに乗っているとこんな感じ、という結果です。

むすび

ざっくり比較するだけでも、それなりに時間がかかってしまいました。

が、初物だけに試してみたかった数字。

粗々でも数字が出せて、俯瞰できた気がしてます。

これから.NET Coreがもっともっと流行っていくことを願ってます。

また、今回のテストはバッチ化してあるので、バージョンアップ時には流してみようかな、とか思ってます。

実際のアプリケーションだと、Trimmedだとうまく動かない等いろいろ出てくるでしょうし、もっと検証は必要ですが、これからも要Watchですね。

あと、PublishSingleFile。しっかりしてくれ。