关于 build-perf-baseline
面向 .NET 开发者的构建性能技能,指导测量冷构建、热构建和空操作构建基线,并通过产物输出、确定性构建、依赖图和并行度优化构建。
主要功能
- 01测量冷、热和空操作基线
- 02使用 binlog 分析性能
- 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— Inputs/Outputs 和最新状态检查build-parallelism— 并行构建和图构建调优eval-performance— glob 和导入链优化
步骤 1:建立性能基线
测量三种场景,了解时间花在了哪里。保持 SDK、 配置、机器、环境变量、还原状态和构建命令一致。每种场景至少运行三次;每次测量前都要重复其准备步骤,并报告中位数及观测范围。单次计时不能构成基线。准备工作应在计时区间之外完成。将每次测量的 binlog 保存为唯一名称,并放在生成的输出目录之外。
冷构建(首次构建)
不存在之前的构建输出。测量包括还原、编译和所有目标在内的完整端到端耗时。
每次冷构建测量前,都要恢复相同的源代码状态,使用被测配置运行 dotnet clean,并仅删除已确认可丢弃的被测项目输出目录和中间目录。除了 bin 和 obj,还要包括自定义构件路径;删除前须获得批准。在下一次构建计时前,确认这些输出不存在。
这是一次输出冷启动构建,并不一定意味着 NuGet、操作系统或编译器服务器缓存是冷的。为所有测量选择并记录一致的缓存/服务器策略;不要清除共享缓存。如果要测量未缓存的还原,请为每次测量使用单独的空软件包缓存。
# 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
不要使用通用的时长或百分比阈值来判定瓶颈。 应依据受控测量结果以及构建自身的目标/任务 耗时对成本进行排序。
捕获分析器证据
binlog 会显示编译器/任务的耗时,但要获取细粒度的分析器耗时,需要运行分析器报告。在 SDK/编译器支持时,捕获:
dotnet build /bl:analyzers.binlog /p:ReportAnalyzer=true
在 MSBuild Structured Log Viewer 中打开 binlog,并检查编译器任务下的分析器摘要。如果无法获取细粒度耗时,可使用 /p:RunAnalyzers=false 对其他条件完全相同的样本进行比较,作为归因实验;不要把禁用分析器当作解决方案。在 CI 中保留分析器强制检查。
记录基线
在优化前后以结构化方式记录基线:
| Scenario | Before | After | Improvement |
|-------------|---------|---------|-------------|
| Cold build | 2m 15s | | |
| Warm build | 1m 40s | | |
| No-op build | 45s | | |
步骤 2:Artifacts 输出布局
UseArtifactsOutput 功能(在 .NET 8 中引入)会更改输出目录结构,以避免 bin/obj 冲突问题并改善缓存效果。
启用 Artifacts 输出
<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 步:其他快速优化
将还原与构建分开
# 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