ARM64_Lab

.NET 9 の ReadyToRun publish は35.03秒かかった

この記事の見出し
  1. publish を二通りに分ける
  2. publish 時間と出力サイズ
  3. 予想と違ったところ
  4. サイズの重さははっきり出た
  5. ビルドのキャッシュも無視できない
  6. 次は起動時間だけを測る

.NET SDK 9.0.316 で win-arm64 向けに publish すると、framework-dependent は1.75秒、self-contained + ReadyToRun は35.03秒だった。

ReadyToRun を付けた self-contained publish は、ビルド時間の代償がかなり大きかった。手元では20倍の差になっているのに、しかも実行時間が変わらなかったのが予想と違ったところで、out-jit も out-r2r も、アプリ内で計測した処理時間は43msだった(publish の重さだけが目立った)。プロセス全体で見ても、温まった後はどちらも0.12秒だった(ここで少し肩透かしを食らった)。

計測日は2026-07-02、手元の記録では2026年7月2日として残している。マシンは Surface Pro 11th Edition、SoC は Snapdragon X Elite X1E80100、OS は Windows 11 Pro 10.0.26200 の ARM64 版。電源は AC 接続で、電源プランは既定のバランスのままにした(この条件を動かすと publish 時間の見え方も変わるかもしれない)。

publish を二通りに分ける

試したのは、dotnet new console で作った小さなコンソールアプリだ。Release ビルドを一度走らせ、同じプロジェクトを win-arm64 向けに2通りで publish している。通常側は framework-dependent、比較側は self-contained にして ReadyToRun を有効にした。

dotnet publish -c Release -r win-arm64 --self-contained false -o out-jit
dotnet publish -c Release -r win-arm64 --self-contained true -p:PublishReadyToRun=true -o out-r2r

実行時間は publish 後の出力ディレクトリから起動して測った。初回だけはディスクやランタイムの読み込みが混ざるので、5回分をそのまま残し、温まった後の値を見ている(初回の数字を捨てるかどうかで印象がかなり変わる)。

Measure-Command { .\out-jit\hello.exe }
Measure-Command { .\out-r2r\hello.exe }

今回は起動だけを測る専用プログラムではなく、処理本体として整数ループを回し、最後に計算結果とアプリ内の経過時間を出す形にしていたので、ここが後で効いてくる話になる(起動差を見たい測定としては寄り道が多い)。

publish 時間と出力サイズ

まずは作成、復元、ビルド、publish の時間と出力サイズを並べる。out-jit のサイズは152852 bytes、5 files。out-r2r は87784078 bytes、189 files だった。self-contained にすると、サイズは574.3倍、ファイル数は37.8倍まで増える。

操作 コマンド elapsed_s 出力サイズ ファイル数
new dotnet new console 2.70秒
restore dotnet restore 1.61秒
build cold dotnet build -c Release 4.08秒
build warm dotnet build -c Release 1.31秒
publish jit framework-dependent win-arm64 1.75秒 152852 bytes 5 files
publish r2r self-contained + ReadyToRun win-arm64 35.03秒 87784078 bytes 189 files

build cold は4.08秒、build warm は1.31秒だった。差は3.1倍ある。restore 単体は1.61秒で、通常の publish 側でも復元が110ms 入っていた。ReadyToRun 側は復元だけで28.09秒かかっており、ここが35.03秒の大部分を持っていった。

次に実行時間を見る。5回分をそのまま置く(初回だけを切り取らないため)。

出力 全5回のプロセス実行時間 最後の標準出力
out-jit 2.48秒 / 0.13秒 / 0.12秒 / 0.12秒 / 0.12秒 119999995 43ms
out-r2r 0.83秒 / 0.12秒 / 0.13秒 / 0.12秒 / 0.12秒 119999995 43ms

初回だけ見ると out-r2r の0.83秒は out-jit の2.48秒より速い。ただし2回目以降はどちらも0.12秒付近に落ち着く。ここだけを日常の自動化に当てはめると、ReadyToRun の勝ちとは言いにくい気がする。

予想と違ったところ

ReadyToRun を付ければ実行時間にも少しは差が出ると思っていた。JIT を減らすのだから、プロセスの立ち上がりが軽くなるはず、という雑な期待があった(名前からして速そうに見えるのもある)。

実際に測った結果は違い、アプリ内の処理時間は out-jit も out-r2r も43ms で同じだった。プロセス全体の温まった値も0.12秒でそろっている。35.03秒かけて publish したのに、今回の見方では実行時の得が見えていない。

これは ReadyToRun が動かなかったという意味ではなく、測り方が悪かったということ。

今回のアプリは整数ループの本体が支配的で、JIT が最初に一度コンパイルしてしまえば、後はネイティブコードが回るだけになる。ReadyToRun の効果は起動時の JIT を省くところに出るはずなのに、処理本体の時間に埋もれる設計にしていた。なぜ起動の差を見たいのに、わざわざループを入れてしまったのか(後から読むと、ここが設計ミスに見える)。

失敗マーカーとしてはここが一番大きい。R2R の良し悪しを見たいなら、短命プロセスを何度も起動する、あるいは起動直後にすぐ終了するコマンドで測るべきだった。今回のループは、私が見たい用途と少しずれている。

サイズの重さははっきり出た

実行時間の差は見えなかったが、出力サイズの差は隠れようがない。framework-dependent の out-jit は152852 bytes と5 files。self-contained + ReadyToRun の out-r2r は87784078 bytes と189 files になった。

この差は、配布方法にそのまま効く。手元で1個だけ動かすなら87,784,078 bytes でも困らないが、タスクごとに小さな CLI をいくつも置く運用では、189 files のディレクトリが増えるのは少し重い(同期対象としても気になる)。OneDrive 配下や Git 管理に置くなら、なおさら扱いが悪くなる(同期対象としても、見た目としても邪魔になる)。

self-contained にはランタイム同梱という利点がある。相手の環境に .NET が入っているかを気にしなくてよい。ただ、このマシンのように .NET SDK 9.0.316 がすでに入っていて、自分の自動化だけを動かすなら、その利点は小さい。サイズ574.3倍を払ってまで選ぶ場面は限られる。

ビルドのキャッシュも無視できない

build cold の4.08秒から build warm の1.31秒まで落ちたのも、個人的には大きい。差は2.77秒ある。小さなコンソールアプリでも、初回と2回目以降では体感が変わった。

restore は単体で1.61秒だったが、通常の publish では復元が110ms に収まっている一方、ReadyToRun 側の publish では復元に28.09秒かかっていた。このあたりは一度だけの現象なのか、self-contained の依存取得が絡んだのか、今回の JSON だけでは切り分けにくい。数字としては出ているが、理由を断定する材料は足りない。

このあたりも、記事にする前にもう一度測り直したくなる場所だ。とはいえ、35.03秒という値が1.75秒と同じ扱いにならないことは十分に分かった。開発中に何度も publish するなら、ReadyToRun は気軽にオンにするものではないだろう。

次は起動時間だけを測る

手元の用途は、自動化から呼ぶ短命 CLI が多い。ファイル名を受け取り、少し変換して、すぐ終わる。そういう使い方なら、見るべきなのは整数ループの処理時間ではなく、プロセス起動から最初の仕事に入るまでの時間だ。

今回の測定では、out-r2r の初回0.83秒と out-jit の初回2.48秒という差は出ている。ただし、それを ReadyToRun の効果として言い切るには弱い。キャッシュ、ディスク、ランタイム読み込み、初回実行の副作用が混ざっている。

だから次は、ほぼ何もしない CLI と、少しだけライブラリを読む CLI を別々の対象にして試してみる。publish 時間、出力サイズ、初回起動、温まった起動を並べて確認したい。今回の結論は、ReadyToRun が遅いというより、ReadyToRun を評価する測定を間違えた、に近いのかもしれない。

それでも運用は少し変わった。普段の開発と自分用 CLI では framework-dependent を基本にする。self-contained + ReadyToRun は、配布先に .NET がない、かつ起動時間だけが本当に効くと分かってから試す。今のところ、35.03秒の publish と87784078 bytes の出力を毎回払う理由は手元では見つかっていない。

a
arm64lab — 個人運営

Surface Pro 11th Edition(Snapdragon X Elite)を2025年5月から常用機にしている個人の記録です。ARM64 版 Windows で詰まったところと、その場で測った値をそのまま書き残しています。特定の企業・団体とは関係がなく、いかなる組織を代表する見解でもありません。