build-perf-baseline について
.NET 開発者向けに、コールド、ウォーム、変更なしのビルド基準を測定し、成果物出力、決定論的ビルド、依存グラフ、並列処理による最適化を案内するスキルです。
主な機能
- 01各種ビルド基準の測定
- 02バイナリログによる性能分析
- 03成果物出力の設定
- 04決定論的ビルドの案内
- 05依存グラフと並列処理の調整
活用シーン
- 01変更前後のビルド性能比較
- 02増分・変更なしビルドの遅延調査
- 03大規模な複数プロジェクト構成の調整
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ dotnet/ skills --skill build-perf-baselineインストール後、Claude Code で/build-perf-baselineを入力して読み込まれたか確認します。
詳細
ビルドパフォーマンスのベースラインと最適化
概要
ビルドを最適化する前に、ベースラインが必要です。測定なしの最適化は当てずっぽうになってしまいます。このスキルでは、ベースラインの確立方法と体系的な最適化手法について説明します。
関連スキル:
build-perf-diagnostics— binlog に基づくボトルネックの特定incremental-build— 入力/出力と最新状態のチェックbuild-parallelism— 並列ビルドとグラフビルドのチューニングeval-performance— glob とインポートチェーンの最適化
ステップ 1:パフォーマンスのベースラインを確立する
時間がどこで使われているかを把握するため、3 つのシナリオを測定します。SDK、構成、マシン、環境変数、復元状態、ビルドコマンドを一貫させてください。各シナリオは少なくとも 3 回実行し、測定サンプルごとにセットアップを繰り返したうえで、中央値と観測された範囲を報告します。1 回の計測時間だけではベースラインになりません。セットアップは計測時間に含めないでください。各サンプルの binlog は一意の名前で、生成された出力ディレクトリの外に保存します。
コールドビルド(初回ビルド)
以前のビルド出力が存在しない状態です。復元、コンパイル、すべてのターゲットを含む、最初から最後までの所要時間を測定します。
すべてのコールドサンプルの前に、同じソース状態を復元し、計測対象の構成で dotnet clean を実行してから、計測対象プロジェクトのうち、削除して問題ないことを確認済みの出力ディレクトリと中間ディレクトリだけを削除します。bin と obj だけでなく、カスタムのアーティファクトパスも対象に含めます。削除前に承認を得てください。次のビルドの計測前に、それらの出力が存在しないことを確認します。
これは出力がコールドなビルドであり、NuGet、OS、コンパイラーサーバーのキャッシュまでコールドとは限りません。すべてのサンプルで一貫するキャッシュ/サーバーポリシーを選び、記録してください。共有キャッシュはクリアしないでください。キャッシュを使わない復元を測定する場合は、サンプルごとに空のパッケージキャッシュを使用します。
# After repeating the cold setup; use a unique log name for each sample
dotnet build /bl:cold-build-1.binlog -m
ウォームビルド(インクリメンタルビルド)
ビルド出力が存在し、一部のファイルに変更がある状態です。インクリメンタルビルドがどれほど効果的に機能するかを測定します。
すべてのウォームサンプルの前に、同じベースラインのソース内容を復元し、計測せずにビルドを正常に完了させます。次に、同じソースファイルに同じ小規模なビルド関連の編集を適用して、ビルドを計測します。変更するファイルと編集内容は、すべてのサンプルで同一にしてください。編集を積み重ねてはいけません。次のサンプルの前にベースラインの内容を復元し、計測時間外に再ビルドします。このセットアップビルドと計測対象のビルドの間でクリーンは実行しないでください。
# Untimed setup after restoring baseline source contents
dotnet build -m
# Apply the same controlled source edit, then measure with a unique log name
dotnet build /bl:warm-build-1.binlog -m
ノーオップビルド(変更なし)
ビルド出力が存在し、何も変更されていない状態です。コンパイルと、正しくインクリメンタルに実行されるターゲットはスキップされます。このビルドの他のサンプルと所要時間を比較します。
すべてのノーオップサンプルの前に、同じベースラインのソース内容を復元し、計測せずにセットアップビルドを正常に完了させます。その後、編集、入力ファイルの更新、出力のクリーン、プロパティの変更を行わずに、同一のビルドを計測します。復元とキャッシュ/サーバーのポリシーは、他のサンプルと一貫させてください。
# Untimed setup after restoring baseline source contents
dotnet build -m
# Rebuild immediately without changes; use a unique log name for each sample
dotnet build /bl:noop-build-1.binlog -m
良好な状態の目安
| シナリオ | 期待される動作 |
|---|---|
| コールドビルド | すべてをコンパイルし、全ターゲットを実行します。これが絶対的なベースラインです |
| ウォームビルド | 変更されたプロジェクトのみを再コンパイルします。変更範囲に比例した時間になります |
| ノーオップビルド | コンパイルと、正しくインクリメンタルに実行されるカスタムターゲットをスキップします。このリポジトリで繰り返し測定したウォームビルドおよびコールドビルドのサンプルと所要時間を比較します |
注意すべき兆候:
- ノーオップの所要時間が繰り返しウォーム/コールドビルドに近い、またはコンパイルターゲットが再実行される → インクリメンタル性を調査します(
incremental-buildを参照) - ウォームビルドで全体が再コンパイルされる → プロジェクトの依存関係チェーンによって全面的な再ビルドが発生しています
- コールドサンプルで復元に時間がかかる → プロジェクト構成を変更する前に、
dotnet restoreとdotnet build --no-restoreを別々に測定します
ボトルネックの判定に、あらゆる状況に当てはまる所要時間や割合のしきい値を使わないでください。管理されたサンプルと、ビルド自体のターゲット/タスクの所要時間を基準に、コストを比較してください。
アナライザーの証拠を取得する
バイナリログにはコンパイラー/タスクのタイミングが記録されますが、詳細なアナライザーのタイミングを取得するには、アナライザーのレポートを有効にして実行する必要があります。SDK/コンパイラーが対応している場合は、次のように記録します。
dotnet build /bl:analyzers.binlog /p:ReportAnalyzer=true
MSBuild Structured Log Viewer でバイナリログを開き、コンパイラータスクの下にあるアナライザーの概要を確認します。詳細なタイミングを取得できない場合は、同一条件のサンプルで /p:RunAnalyzers=false を指定し、原因を切り分けるための実験として比較します。アナライザーを無効にすることを解決策として提示しないでください。CI ではアナライザーの適用を維持します。
ベースラインの記録
最適化の前後で、次のように構造化してベースラインを記録します。
| Scenario | Before | After | Improvement |
|-------------|---------|---------|-------------|
| Cold build | 2m 15s | | |
| Warm build | 1m 40s | | |
| No-op build | 45s | | |
ステップ 2:成果物の出力レイアウト
UseArtifactsOutput 機能(.NET 8 で導入)は、出力ディレクトリの構造を変更し、bin/obj の競合を回避して、キャッシュを改善します。
Artifacts Output の有効化
<PropertyGroup>
<UseArtifactsOutput>true</UseArtifactsOutput>
</PropertyGroup>
変更前と変更後
# Traditional layout (before)
src/
MyLib/
bin/Debug/net8.0/MyLib.dll
obj/Debug/net8.0/...
MyApp/
bin/Debug/net8.0/MyApp.dll
# Artifacts layout (after)
artifacts/
bin/MyLib/debug/MyLib.dll
bin/MyApp/debug/MyApp.dll
obj/MyLib/debug/...
obj/MyApp/debug/...
メリット
- bin/obj の競合なし:各プロジェクトと構成に対して、一意のパスが自動的に割り当てられます
- キャッシュが容易:CI でキャッシュ/復元するディレクトリを
artifacts/に集約できます - .gitignore がすっきり:
artifacts/を無視するだけです - マルチターゲットに対応:各 TFM に専用のサブディレクトリが割り当てられます
カスタマイズ
<PropertyGroup>
<ArtifactsPath>$(MSBuildThisFileDirectory)output</ArtifactsPath>
</PropertyGroup>
ステップ 3:決定的なビルド
決定的なビルドでは、入力が同じであればバイト単位で同一の出力が生成されます。これはビルドキャッシュと再現性のために不可欠です。
決定的なビルドの有効化
<PropertyGroup>
<Deterministic>true</Deterministic>
<ContinuousIntegrationBuild Condition="'$(CI)' == 'true'">true</ContinuousIntegrationBuild>
</PropertyGroup>
決定性により変わること
- PE ヘッダーからタイムスタンプが削除されます
- PDB で一貫したファイルパスが使用されます
- 同一の入力に対して同一の出力が生成されます
パフォーマンス上の重要性
- ビルドキャッシュ:出力が決定的であれば、ビルド間やマシン間でキャッシュして再利用できます
- CI の最適化:入力を比較して、変更されていないプロジェクトの再ビルドをスキップできます
- 分散ビルド:コンパイル結果を共有ストレージに安全にキャッシュできます
ステップ 4:依存関係グラフの整理
不要なプロジェクト参照を減らすと、クリティカルパスが短縮され、ビルド対象も減ります。
依存関係グラフの監査
# Visualize the dependency graph
dotnet build /bl:graph.binlog
# In the binlog, check project references and build times
# Look for projects that are referenced but could be trimmed
手法
冗長な推移的参照を削除する
<ItemGroup>
<ProjectReference Include="..\Core\Core.csproj" />
<ProjectReference Include="..\Utils\Utils.csproj" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Core\Core.csproj" />
</ItemGroup>
ビルド順序のみを指定する参照
プロジェクトが自分のプロジェクトより先にビルドされる必要があるものの、そのアセンブリ出力は必要ない場合:
<ProjectReference Include="..\CodeGen\CodeGen.csproj"
ReferenceOutputAssembly="false" />
推移的な伝播を防止する
依存関係が内部実装の詳細であり、利用側に伝播させるべきでない場合:
<ProjectReference Include="..\InternalHelpers\InternalHelpers.csproj"
PrivateAssets="all" />
推移的なプロジェクト参照を無効にする
明示的な依存関係管理を行う場合(非常に大規模なリポジトリでの極端な手段):
<PropertyGroup>
<DisableTransitiveProjectReferences>true</DisableTransitiveProjectReferences>
</PropertyGroup>
注意:すべての依存関係を明示的に列挙する必要があります。推移的閉包によって過剰な再ビルドが発生している、大規模なリポジトリでのみ使用してください。
ステップ 5:静的グラフビルド(/graph)
静的グラフモードでは、ビルド前にプロジェクトグラフ全体を評価するため、スケジューリングと分離が改善されます。
グラフビルドの有効化
# Single invocation
dotnet build /graph
# With binary log for analysis
dotnet build /graph /bl:graph-build.binlog
メリット
- 並列性の向上:MSBuild が事前にグラフ全体を把握できるため、最適なスケジュールを設定できます
- ビルドの分離:各プロジェクトが分離された状態でビルドされます(プロジェクト間で状態が漏れません)
- キャッシュの可能性:分離により、プロジェクトごとの結果をキャッシュできます
使用する場面
| シナリオ | 推奨事項 |
|---|---|
| 大規模な複数プロジェクトのソリューション(20 以上のプロジェクト) | ✅ /graph を試してください。並列性が大幅に向上する可能性があります |
| 小規模なソリューション(5 未満のプロジェクト) | ❌ グラフ評価のオーバーヘッドがメリットを上回ります |
| CI ビルド | ✅ グラフビルドはより予測しやすく、並列化しやすいです |
| ローカル開発 | ⚠️ どちらもテストしてください。プロジェクト構成によって効果がある場合とない場合があります |
グラフビルドのトラブルシューティング
グラフビルドでは、すべての ProjectReference 項目を静的に判定できる必要があります(ターゲットで動的に計算される参照は使用できません)。グラフビルドに失敗する場合:
error MSB4260: Project reference "..." could not be resolved with static graph.
修正方法:すべての ProjectReference 項目を、ターゲットの外側にある <ItemGroup> で宣言してください(<Target> ブロック内で動的に計算しないでください)。
ステップ 6:並列ビルドの調整
MaxCpuCount
# Use all available cores (default in dotnet build)
dotnet build -m
# Specify explicit core count (useful for CI with shared agents)
dotnet build -m:4
# MSBuild.exe syntax
msbuild /m:8 MySolution.sln
並列処理のボトルネックを特定する
binlog で、次の点を確認します。
- 長い逐次チェーン:依存関係のため、プロジェクトを順番にビルドしなければならない状態
- 負荷の偏り:一部のビルドノードがアイドル状態の一方で、ほかのノードが過負荷になっている状態
- 単一プロジェクトのボトルネック:クリティカルパス上にある大規模プロジェクトが、すべての処理をブロックしている状態
binlog の分析では、grep 'Target Performance Summary' -A 30 full.log を使ってビルドノードの使用状況を確認します。
クリティカルパスを短縮する
クリティカルパスは、依存関係のあるプロジェクトが連なる最長のチェーンです。短縮するには、次の方法があります。
- 大規模なプロジェクトを小さなプロジェクトに分割し、並列ビルドできるようにします
- 不要な ProjectReferences を削除します(ステップ 5 を参照)
- ビルド順序だけを指定する依存関係には、
ReferenceOutputAssembly="false"を使用します - 共有コードをベースライブラリに移動し、先にビルドしてから、それを使用するプロジェクトを並列化します
ステップ 7:そのほかの手軽な改善策
Restore と Build を分ける
# In CI, restore once then build without restore
dotnet restore
dotnet build --no-restore -m
dotnet test --no-build
不要なターゲットをスキップする
# Skip building documentation
dotnet build /p:GenerateDocumentationFile=false
# Attribution experiment only: compare against the same build with analyzers
dotnet build /p:RunAnalyzers=false
構成を変更する前に、これらのスイッチを使用して寄与度を測定します。
このベースライン測定の段階では、アナライザーを恒久的に無効にすることを推奨しないでください。
測定されたアナライザーのボトルネックは build-perf-diagnostics に引き継ぎ、
CI の適用を維持します。
プロジェクト単位でフィルタリングする
# Build only the project you're working on (and its dependencies)
dotnet build src/MyApp/MyApp.csproj
# Don't build the entire solution if you only need one project
すべての調査でバイナリログを使用する
必ず binlog から始めます。
dotnet build /bl:perf.binlog -m
次に、build-perf-diagnostics スキルと binlog ツールを使って、ボトルネックを体系的に特定します。
最適化の判断ツリー
Is your repeated no-op build disproportionately close to warm/cold samples,
or are compile/custom targets rerunning?
├── YES → See `incremental-build` skill (inspect Inputs/Outputs and skip reasons)
└── NO
Is your cold build slow?
├── YES
│ Is restore slow?
│ ├── YES → Optimize NuGet restore (use lock files, configure local cache)
│ └── NO
│ Is compilation slow?
│ ├── YES
│ │ Are analyzers/generators slow?
│ │ ├── YES → See `build-perf-diagnostics` skill
│ │ └── NO → Check parallelism, graph build, critical path (this skill + `build-parallelism`)
│ └── NO → Check custom targets (binlog analysis via `build-perf-diagnostics`)
└── NO
Is your warm build slow?
├── YES → Projects rebuilding unnecessarily → check `incremental-build` skill
└── NO → Baseline is healthy; adopt graph build or UseArtifactsOutput only for a measured need