build-perf-baseline
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseBuild Performance Baseline & Optimization
构建性能基线与优化
Overview
概述
Before optimizing a build, you need a baseline. Without measurements, optimization is guesswork. This skill covers how to establish baselines and apply systematic optimization techniques.
Related skills:
- — binlog-based bottleneck identification
build-perf-diagnostics - — Inputs/Outputs and up-to-date checks
incremental-build - — parallel and graph build tuning
build-parallelism - — glob and import chain optimization
eval-performance
优化构建前,你需要先确立基线。没有量化测量,优化就只是盲目的猜测。本技能涵盖建立性能基线和应用系统化优化技术的完整方法。
相关技能:
- — 基于binlog的瓶颈识别
build-perf-diagnostics - — 输入/输出与最新状态检查
incremental-build - — 并行构建与图构建调优
build-parallelism - — glob匹配与导入链优化
eval-performance
Step 1: Establish a Performance Baseline
步骤1:建立性能基线
Measure three scenarios to understand where time is spent:
测量三个场景的耗时,明确时间消耗分布:
Cold Build (First Build)
冷构建(首次构建)
No previous build output exists. Measures the full end-to-end time including restore, compilation, and all targets.
bash
undefined无任何历史构建输出,测量从还原、编译到所有目标执行的完整端到端耗时。
bash
undefinedClean everything first
先清理所有内容
dotnet clean
dotnet clean
Remove bin/obj to truly start fresh
彻底删除bin/obj目录保证从零开始
Get-ChildItem -Recurse -Directory -Include bin,obj | Remove-Item -Recurse -Force
Get-ChildItem -Recurse -Directory -Include bin,obj | Remove-Item -Recurse -Force
OR on Linux/macOS:
Linux/macOS 环境使用以下命令:
find . -type d ( -name bin -o -name obj ) -exec rm -rf {} +
find . -type d ( -name bin -o -name obj ) -exec rm -rf {} +
Measure cold build
测量冷构建耗时
dotnet build /bl:cold-build.binlog -m
undefineddotnet build /bl:cold-build.binlog -m
undefinedWarm Build (Incremental Build)
暖构建(增量构建)
Build output exists, some files have changed. Measures how well incremental build works.
bash
undefined已存在构建输出,仅修改了部分文件,用于衡量增量构建的生效效果。
bash
undefinedBuild once to populate outputs
先构建一次生成所有输出
dotnet build -m
dotnet build -m
Make a small change (touch one .cs file)
做一个小修改(比如修改任意一个.cs文件)
Then rebuild
然后重新构建
dotnet build /bl:warm-build.binlog -m
undefineddotnet build /bl:warm-build.binlog -m
undefinedNo-Op Build (Nothing Changed)
无操作构建(无任何改动)
Build output exists, nothing has changed. This should be nearly instant. If it's slow, incremental build is broken.
bash
undefined已存在构建输出,且没有任何文件变更,理论上应该接近即时完成。如果耗时过长,说明增量构建存在问题。
bash
undefinedBuild once to populate outputs
先构建一次生成所有输出
dotnet build -m
dotnet build -m
Rebuild immediately without changes
不做任何修改立即重新构建
dotnet build /bl:noop-build.binlog -m
undefineddotnet build /bl:noop-build.binlog -m
undefinedWhat Good Looks Like
合格标准
| Scenario | Expected Behavior |
|---|---|
| Cold build | Full compilation, all targets run. This is your absolute baseline |
| Warm build | Only changed projects recompile. Time proportional to change scope |
| No-op build | < 5 seconds for small repos, < 30 seconds for large repos. All compilation targets should report "Skipping target — all outputs up-to-date" |
Red flags:
- No-op build > 30 seconds → incremental build is broken (see skill)
incremental-build - Warm build recompiles everything → project dependency chain forces full rebuild
- Cold build has long restore → NuGet cache issues
| 场景 | 预期表现 |
|---|---|
| 冷构建 | 执行全量编译与所有目标,作为你的绝对基准线 |
| 暖构建 | 仅重编译变更的项目,耗时与变更规模成正比 |
| 无操作构建 | 小型仓库耗时<5秒,大型仓库<30秒,所有编译目标都应该提示「跳过目标——所有输出均为最新」 |
风险信号:
- 无操作构建耗时>30秒 → 增量构建失效(参考技能)
incremental-build - 暖构建重编译所有项目 → 项目依赖链强制触发全量重建
- 冷构建还原阶段耗时过长 → NuGet缓存存在问题
Recording Baselines
记录基线
Record baselines in a structured way before and after optimization:
| Scenario | Before | After | Improvement |
|-------------|---------|---------|-------------|
| Cold build | 2m 15s | | |
| Warm build | 1m 40s | | |
| No-op build | 45s | | |在优化前后结构化记录基线数据:
| 场景 | 优化前 | 优化后 | 提升幅度 |
|-------------|---------|---------|-------------|
| 冷构建 | 2分15秒 | | |
| 暖构建 | 1分40秒 | | |
| 无操作构建 | 45秒 | | |Step 2: MSBuild Server (Persistent Build Process)
步骤2:MSBuild Server(持久化构建进程)
The MSBuild server keeps the build process alive between invocations, avoiding JIT compilation and assembly loading overhead on every build.
MSBuild Server会在多次构建调用之间保留构建进程,避免每次构建都重复执行JIT编译和程序集加载的开销。
Enabling MSBuild Server
启用MSBuild Server
bash
undefinedbash
undefinedEnabled by default in .NET 8+ but can be forced
.NET 8+默认已启用,可手动强制开启
dotnet build /p:UseSharedCompilation=true
The MSBuild server is started automatically and reused across builds. The compiler server (VBCSCompiler / `dotnet build-server`) is separate but complementary.dotnet build /p:UseSharedCompilation=true
MSBuild Server会自动启动并在多次构建间复用,编译器服务(VBCSCompiler / `dotnet build-server`)是独立但互补的组件。Managing the Build Server
管理构建服务
bash
undefinedbash
undefinedCheck if the server is running
检查服务是否运行
dotnet build-server status
dotnet build-server status
Shut down all build servers (useful when debugging)
关闭所有构建服务(调试时非常有用)
dotnet build-server shutdown
undefineddotnet build-server shutdown
undefinedWhen to Restart the Build Server
何时需要重启构建服务
Restart after:
- Updating the .NET SDK
- Changing MSBuild tooling (custom tasks, props, targets)
- Debugging build infrastructure issues
- Seeing stale behavior in repeated builds
bash
dotnet build-server shutdown
dotnet build在以下场景后需要重启:
- 更新.NET SDK后
- 修改MSBuild工具链(自定义任务、props、targets文件)后
- 调试构建基础设施问题时
- 多次构建出现 stale 异常行为时
bash
dotnet build-server shutdown
dotnet buildStep 3: Artifacts Output Layout
步骤3:产物输出布局
The feature (introduced in .NET 8) changes the output directory structure to avoid bin/obj clash issues and enable better caching.
UseArtifactsOutput.NET 8引入的特性修改了输出目录结构,避免bin/obj冲突问题,同时优化缓存效果。
UseArtifactsOutputEnabling Artifacts Output
启用产物输出
xml
<!-- Directory.Build.props -->
<PropertyGroup>
<UseArtifactsOutput>true</UseArtifactsOutput>
</PropertyGroup>xml
<!-- Directory.Build.props -->
<PropertyGroup>
<UseArtifactsOutput>true</UseArtifactsOutput>
</PropertyGroup>Before vs After
新旧布局对比
undefinedundefinedTraditional layout (before)
传统布局(优化前)
src/
MyLib/
bin/Debug/net8.0/MyLib.dll
obj/Debug/net8.0/...
MyApp/
bin/Debug/net8.0/MyApp.dll
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/...
undefinedartifacts/
bin/MyLib/debug/MyLib.dll
bin/MyApp/debug/MyApp.dll
obj/MyLib/debug/...
obj/MyApp/debug/...
undefinedBenefits
优势
- No bin/obj clash: Each project+configuration gets a unique path automatically
- Easier to cache: Single directory to cache/restore in CI
artifacts/ - Cleaner .gitignore: Just ignore
artifacts/ - Multi-targeting safe: Each TFM gets its own subdirectory
- 无bin/obj冲突:每个项目+配置组合自动分配唯一路径
- 缓存更便捷:CI中仅需缓存/恢复单个目录即可
artifacts/ - .gitignore更简洁:仅需忽略目录
artifacts/ - 多目标框架安全:每个TFM都有独立的子目录
Customizing
自定义配置
xml
<!-- Change the artifacts root -->
<PropertyGroup>
<ArtifactsPath>$(MSBuildThisFileDirectory)output</ArtifactsPath>
</PropertyGroup>xml
<!-- 修改产物根目录 -->
<PropertyGroup>
<ArtifactsPath>$(MSBuildThisFileDirectory)output</ArtifactsPath>
</PropertyGroup>Step 4: Deterministic Builds
步骤4:确定性构建
Deterministic builds produce byte-for-byte identical output given the same inputs. This is essential for build caching and reproducibility.
确定性构建可以保证相同输入生成完全一致的字节输出,是构建缓存和可复现性的基础。
Enabling Deterministic Builds
启用确定性构建
xml
<!-- Directory.Build.props -->
<PropertyGroup>
<!-- Enabled by default in .NET SDK projects since SDK 2.0+ -->
<Deterministic>true</Deterministic>
<!-- For full reproducibility, also set: -->
<ContinuousIntegrationBuild Condition="'$(CI)' == 'true'">true</ContinuousIntegrationBuild>
</PropertyGroup>xml
<!-- Directory.Build.props -->
<PropertyGroup>
<!-- .NET SDK 2.0+版本的项目默认已启用 -->
<Deterministic>true</Deterministic>
<!-- 如需完全可复现,还需要配置: -->
<ContinuousIntegrationBuild Condition="'$(CI)' == 'true'">true</ContinuousIntegrationBuild>
</PropertyGroup>What Deterministic Affects
确定性构建的影响
- Removes timestamps from PE headers
- Uses consistent file paths in PDBs
- Produces identical output for identical input
- 移除PE头中的时间戳
- 在PDB中使用一致的文件路径
- 相同输入永远生成相同输出
Why It Matters for Performance
对性能的价值
- Build caching: If outputs are deterministic, you can cache and reuse them across builds and machines
- CI optimization: Skip rebuilding unchanged projects by comparing inputs
- Distributed builds: Safe to cache compilation results in shared storage
- 构建缓存:如果输出是确定的,你可以跨构建、跨机器缓存复用构建产物
- CI优化:通过对比输入即可跳过未变更项目的构建
- 分布式构建:可以安全地在共享存储中缓存编译结果
Step 5: Dependency Graph Trimming
步骤5:依赖图裁剪
Reducing unnecessary project references shortens the critical path and reduces what gets built.
减少不必要的项目引用可以缩短关键路径,减少需要构建的内容。
Audit the Dependency Graph
审计依赖图
bash
undefinedbash
undefinedVisualize the dependency graph
可视化依赖图
dotnet build /bl:graph.binlog
dotnet build /bl:graph.binlog
In the binlog, check project references and build times
在binlog中检查项目引用和构建耗时
Look for projects that are referenced but could be trimmed
寻找可以裁剪的不必要引用
undefinedundefinedTechniques
优化技巧
Remove Redundant Transitive References
移除冗余的传递引用
xml
<!-- BAD: Utils is already referenced transitively via Core -->
<ItemGroup>
<ProjectReference Include="..\Core\Core.csproj" />
<ProjectReference Include="..\Utils\Utils.csproj" />
</ItemGroup>
<!-- GOOD: Let transitive references flow automatically -->
<ItemGroup>
<ProjectReference Include="..\Core\Core.csproj" />
</ItemGroup>xml
<!-- 错误写法:Utils已经通过Core被传递引用了 -->
<ItemGroup>
<ProjectReference Include="..\Core\Core.csproj" />
<ProjectReference Include="..\Utils\Utils.csproj" />
</ItemGroup>
<!-- 正确写法:让传递引用自动生效 -->
<ItemGroup>
<ProjectReference Include="..\Core\Core.csproj" />
</ItemGroup>Build-Order-Only References
仅构建顺序依赖
When you need a project to build before yours but don't need its assembly output:
xml
<!-- Only ensures build order, doesn't reference the output assembly -->
<ProjectReference Include="..\CodeGen\CodeGen.csproj"
ReferenceOutputAssembly="false" />如果你只需要某个项目先构建,但不需要引用它的程序集输出:
xml
<!-- 仅保证构建顺序,不引用输出程序集 -->
<ProjectReference Include="..\CodeGen\CodeGen.csproj"
ReferenceOutputAssembly="false" />Prevent Transitive Flow
阻止传递引用外泄
When a dependency is an internal implementation detail that shouldn't flow to consumers:
xml
<!-- Don't expose this dependency transitively -->
<ProjectReference Include="..\InternalHelpers\InternalHelpers.csproj"
PrivateAssets="all" />当某个依赖是内部实现细节,不应该暴露给消费者时:
xml
<!-- 不对外暴露这个传递依赖 -->
<ProjectReference Include="..\InternalHelpers\InternalHelpers.csproj"
PrivateAssets="all" />Disable Transitive Project References
禁用传递项目引用
For explicit-only dependency management (extreme measure for very large repos):
xml
<PropertyGroup>
<DisableTransitiveProjectReferences>true</DisableTransitiveProjectReferences>
</PropertyGroup>Caution: This requires all dependencies to be listed explicitly. Only use in large repos where transitive closure is causing excessive rebuilds.
用于仅允许显式声明依赖的场景(超大型仓库的极端优化手段):
xml
<PropertyGroup>
<DisableTransitiveProjectReferences>true</DisableTransitiveProjectReferences>
</PropertyGroup>注意:该配置要求所有依赖都必须显式声明,仅适用于传递闭包导致过量重建的大型仓库。
Step 6: Static Graph Builds (/graph
)
/graph步骤6:静态图构建(/graph
)
/graphStatic graph mode evaluates the entire project graph before building, enabling better scheduling and isolation.
静态图模式会在构建前先评估完整的项目图,实现更优的调度和隔离。
Enabling Graph Build
启用图构建
bash
undefinedbash
undefinedSingle invocation
单次调用启用
dotnet build /graph
dotnet build /graph
With binary log for analysis
同时生成二进制日志用于分析
dotnet build /graph /bl:graph-build.binlog
undefineddotnet build /graph /bl:graph-build.binlog
undefinedBenefits
优势
- Better parallelism: MSBuild knows the full graph upfront and can schedule optimally
- Build isolation: Each project builds in isolation (no cross-project state leakage)
- Caching potential: With isolation, individual project results can be cached
- 更优的并行度:MSBuild提前掌握完整依赖图,可以做最优调度
- 构建隔离:每个项目独立构建,没有跨项目状态泄漏
- 更高的缓存潜力:隔离环境下单个项目的构建结果可以被缓存
When to Use
适用场景
| Scenario | Recommendation |
|---|---|
| Large multi-project solution (20+ projects) | ✅ Try |
| Small solution (< 5 projects) | ❌ Overhead of graph evaluation outweighs benefits |
| CI builds | ✅ Graph builds are more predictable and parallelizable |
| Local development | ⚠️ Test both — may or may not help depending on project structure |
| 场景 | 推荐度 |
|---|---|
| 大型多项目解决方案(20+项目) | ✅ 推荐使用 |
| 小型解决方案(<5个项目) | ❌ 图评估的开销大于收益 |
| CI构建 | ✅ 图构建更可预测,并行性更好 |
| 本地开发 | ⚠️ 自行测试效果,是否生效取决于项目结构 |
Troubleshooting Graph Build
图构建问题排查
Graph build requires that all items are statically determinable (no dynamic references computed in targets). If graph build fails:
ProjectReferenceerror MSB4260: Project reference "..." could not be resolved with static graph.Fix: Ensure all items are declared in outside of targets (not dynamically computed inside blocks).
ProjectReference<ItemGroup><Target>图构建要求所有都是静态可确定的(不能在target中动态计算引用)。如果构建失败:
ProjectReferenceerror MSB4260: Project reference "..." could not be resolved with static graph.修复方案:确保所有都在target外的中声明,不要在块内动态计算。
ProjectReference<ItemGroup><Target>Step 7: Parallel Build Tuning
步骤7:并行构建调优
MaxCpuCount
MaxCpuCount配置
bash
undefinedbash
undefinedUse all available cores (default in dotnet build)
使用所有可用核心(dotnet build默认行为)
dotnet build -m
dotnet build -m
Specify explicit core count (useful for CI with shared agents)
指定核心数(适用于共享代理的CI环境)
dotnet build -m:4
dotnet build -m:4
MSBuild.exe syntax
MSBuild.exe语法
msbuild /m:8 MySolution.sln
undefinedmsbuild /m:8 MySolution.sln
undefinedIdentifying Parallelism Bottlenecks
识别并行瓶颈
In a binlog, look for:
- Long sequential chains: Projects that must build one after another due to dependencies
- Uneven load: Some build nodes idle while others are overloaded
- Single-project bottleneck: One large project on the critical path that blocks everything
Use in binlog analysis to see build node utilization.
grep 'Target Performance Summary' -A 30 full.log在binlog中查找以下问题:
- 长串行链:由于依赖关系必须依次构建的项目链
- 负载不均:部分构建节点空闲,其他节点过载
- 单项目瓶颈:关键路径上的单个大型项目阻塞所有后续构建
可以使用分析binlog,查看构建节点利用率。
grep 'Target Performance Summary' -A 30 full.logReducing the Critical Path
缩短关键路径
The critical path is the longest chain of dependent projects. To shorten it:
- Break large projects into smaller ones that can build in parallel
- Remove unnecessary ProjectReferences (see Step 5)
- Use for build-order-only dependencies
ReferenceOutputAssembly="false" - Move shared code to a base library that builds first, then parallelize consumers
关键路径是依赖项目的最长链,可以通过以下方式缩短:
- 拆分大型项目为更小的模块,实现并行构建
- 移除不必要的ProjectReference(参考步骤5)
- 对仅构建顺序依赖使用
ReferenceOutputAssembly="false" - 将共享代码迁移到优先构建的基础库,后续消费者可以并行构建
Step 8: Additional Quick Wins
步骤8:额外快速优化项
Separate Restore from Build
分离还原与构建流程
bash
undefinedbash
undefinedIn CI, restore once then build without restore
CI环境中先执行一次还原,后续构建跳过还原步骤
dotnet restore
dotnet build --no-restore -m
dotnet test --no-build
undefineddotnet restore
dotnet build --no-restore -m
dotnet test --no-build
undefinedSkip Unnecessary Targets
跳过不必要的构建目标
bash
undefinedbash
undefinedSkip building documentation
跳过文档生成
dotnet build /p:GenerateDocumentationFile=false
dotnet build /p:GenerateDocumentationFile=false
Skip analyzers during development (not for CI!)
开发阶段跳过分析器(CI环境不要用!)
dotnet build /p:RunAnalyzers=false
undefineddotnet build /p:RunAnalyzers=false
undefinedUse Project-Level Filtering
使用项目级过滤
bash
undefinedbash
undefinedBuild only the project you're working on (and its dependencies)
仅构建你正在开发的项目(及其依赖)
dotnet build src/MyApp/MyApp.csproj
dotnet build src/MyApp/MyApp.csproj
Don't build the entire solution if you only need one project
不需要单个项目时不要构建整个解决方案
undefinedundefinedBinary Log for All Investigations
所有排查都从二进制日志开始
Always start with a binlog:
bash
dotnet build /bl:perf.binlog -mThen use the skill and binlog tools for systematic bottleneck identification.
build-perf-diagnostics任何性能问题排查都先生成binlog:
bash
dotnet build /bl:perf.binlog -m然后使用技能和binlog工具做系统化的瓶颈识别。
build-perf-diagnosticsOptimization Decision Tree
优化决策树
Is your no-op build slow (> 10s per project)?
├── YES → See `incremental-build` skill (fix Inputs/Outputs)
└── 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 → Build is healthy! Consider graph build or UseArtifactsOutput for further gains无操作构建是否过慢(每个项目>10秒)?
├── 是 → 参考`incremental-build`技能(修复输入/输出配置)
└── 否
冷构建是否过慢?
├── 是
│ 还原阶段是否过慢?
│ ├── 是 → 优化NuGet还原(使用锁文件、配置本地缓存)
│ └── 否
│ 编译阶段是否过慢?
│ ├── 是
│ │ 分析器/代码生成器是否过慢?
│ │ ├── 是 → 参考`build-perf-diagnostics`技能
│ │ └── 否 → 检查并行度、图构建、关键路径(本技能 + `build-parallelism`)
│ └── 否 → 检查自定义目标(通过`build-perf-diagnostics`做binlog分析)
└── 否
暖构建是否过慢?
├── 是 → 项目存在不必要的重建 → 参考`incremental-build`技能
└── 否 → 构建状态健康!可以尝试图构建或UseArtifactsOutput做进一步优化