build-perf-baseline

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Build 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:
  • build-perf-diagnostics
    — binlog-based bottleneck identification
  • incremental-build
    — Inputs/Outputs and up-to-date checks
  • build-parallelism
    — parallel and graph build tuning
  • eval-performance
    — glob and import chain optimization

优化构建前,你需要先确立基线。没有量化测量,优化就只是盲目的猜测。本技能涵盖建立性能基线和应用系统化优化技术的完整方法。
相关技能:
  • build-perf-diagnostics
    — 基于binlog的瓶颈识别
  • incremental-build
    — 输入/输出与最新状态检查
  • build-parallelism
    — 并行构建与图构建调优
  • eval-performance
    — glob匹配与导入链优化

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
undefined

Clean 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
undefined
dotnet build /bl:cold-build.binlog -m
undefined

Warm Build (Incremental Build)

暖构建(增量构建)

Build output exists, some files have changed. Measures how well incremental build works.
bash
undefined
已存在构建输出,仅修改了部分文件,用于衡量增量构建的生效效果。
bash
undefined

Build 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
undefined
dotnet build /bl:warm-build.binlog -m
undefined

No-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
undefined

Build once to populate outputs

先构建一次生成所有输出

dotnet build -m
dotnet build -m

Rebuild immediately without changes

不做任何修改立即重新构建

dotnet build /bl:noop-build.binlog -m
undefined
dotnet build /bl:noop-build.binlog -m
undefined

What Good Looks Like

合格标准

ScenarioExpected Behavior
Cold buildFull compilation, all targets run. This is your absolute baseline
Warm buildOnly 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
    incremental-build
    skill)
  • 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
undefined
bash
undefined

Enabled 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
undefined
bash
undefined

Check 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
undefined
dotnet build-server shutdown
undefined

When 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 build

Step 3: Artifacts Output Layout

步骤3:产物输出布局

The
UseArtifactsOutput
feature (introduced in .NET 8) changes the output directory structure to avoid bin/obj clash issues and enable better caching.
.NET 8引入的
UseArtifactsOutput
特性修改了输出目录结构,避免bin/obj冲突问题,同时优化缓存效果。

Enabling Artifacts Output

启用产物输出

xml
<!-- Directory.Build.props -->
<PropertyGroup>
  <UseArtifactsOutput>true</UseArtifactsOutput>
</PropertyGroup>
xml
<!-- Directory.Build.props -->
<PropertyGroup>
  <UseArtifactsOutput>true</UseArtifactsOutput>
</PropertyGroup>

Before vs After

新旧布局对比

undefined
undefined

Traditional 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/...
undefined
artifacts/ bin/MyLib/debug/MyLib.dll bin/MyApp/debug/MyApp.dll obj/MyLib/debug/... obj/MyApp/debug/...
undefined

Benefits

优势

  • No bin/obj clash: Each project+configuration gets a unique path automatically
  • Easier to cache: Single
    artifacts/
    directory to cache/restore in CI
  • 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
undefined
bash
undefined

Visualize 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

寻找可以裁剪的不必要引用

undefined
undefined

Techniques

优化技巧

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
)

步骤6:静态图构建(
/graph

Static graph mode evaluates the entire project graph before building, enabling better scheduling and isolation.
静态图模式会在构建前先评估完整的项目图,实现更优的调度和隔离。

Enabling Graph Build

启用图构建

bash
undefined
bash
undefined

Single invocation

单次调用启用

dotnet build /graph
dotnet build /graph

With binary log for analysis

同时生成二进制日志用于分析

dotnet build /graph /bl:graph-build.binlog
undefined
dotnet build /graph /bl:graph-build.binlog
undefined

Benefits

优势

  • 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

适用场景

ScenarioRecommendation
Large multi-project solution (20+ projects)✅ Try
/graph
— may see significant parallelism gains
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+项目)✅ 推荐使用
/graph
,可能获得显著的并行效率提升
小型解决方案(<5个项目)❌ 图评估的开销大于收益
CI构建✅ 图构建更可预测,并行性更好
本地开发⚠️ 自行测试效果,是否生效取决于项目结构

Troubleshooting Graph Build

图构建问题排查

Graph build requires that all
ProjectReference
items are statically determinable (no dynamic references computed in targets). If graph build fails:
error MSB4260: Project reference "..." could not be resolved with static graph.
Fix: Ensure all
ProjectReference
items are declared in
<ItemGroup>
outside of targets (not dynamically computed inside
<Target>
blocks).

图构建要求所有
ProjectReference
都是静态可确定的(不能在target中动态计算引用)。如果构建失败:
error MSB4260: Project reference "..." could not be resolved with static graph.
修复方案:确保所有
ProjectReference
都在target外的
<ItemGroup>
中声明,不要在
<Target>
块内动态计算。

Step 7: Parallel Build Tuning

步骤7:并行构建调优

MaxCpuCount

MaxCpuCount配置

bash
undefined
bash
undefined

Use 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
undefined
msbuild /m:8 MySolution.sln
undefined

Identifying 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
grep 'Target Performance Summary' -A 30 full.log
in binlog analysis to see build node utilization.
在binlog中查找以下问题:
  • 长串行链:由于依赖关系必须依次构建的项目链
  • 负载不均:部分构建节点空闲,其他节点过载
  • 单项目瓶颈:关键路径上的单个大型项目阻塞所有后续构建
可以使用
grep 'Target Performance Summary' -A 30 full.log
分析binlog,查看构建节点利用率。

Reducing the Critical Path

缩短关键路径

The critical path is the longest chain of dependent projects. To shorten it:
  1. Break large projects into smaller ones that can build in parallel
  2. Remove unnecessary ProjectReferences (see Step 5)
  3. Use
    ReferenceOutputAssembly="false"
    for build-order-only dependencies
  4. Move shared code to a base library that builds first, then parallelize consumers

关键路径是依赖项目的最长链,可以通过以下方式缩短:
  1. 拆分大型项目为更小的模块,实现并行构建
  2. 移除不必要的ProjectReference(参考步骤5)
  3. 对仅构建顺序依赖使用
    ReferenceOutputAssembly="false"
  4. 将共享代码迁移到优先构建的基础库,后续消费者可以并行构建

Step 8: Additional Quick Wins

步骤8:额外快速优化项

Separate Restore from Build

分离还原与构建流程

bash
undefined
bash
undefined

In CI, restore once then build without restore

CI环境中先执行一次还原,后续构建跳过还原步骤

dotnet restore dotnet build --no-restore -m dotnet test --no-build
undefined
dotnet restore dotnet build --no-restore -m dotnet test --no-build
undefined

Skip Unnecessary Targets

跳过不必要的构建目标

bash
undefined
bash
undefined

Skip 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
undefined
dotnet build /p:RunAnalyzers=false
undefined

Use Project-Level Filtering

使用项目级过滤

bash
undefined
bash
undefined

Build 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

不需要单个项目时不要构建整个解决方案

undefined
undefined

Binary Log for All Investigations

所有排查都从二进制日志开始

Always start with a binlog:
bash
dotnet build /bl:perf.binlog -m
Then use the
build-perf-diagnostics
skill and binlog tools for systematic bottleneck identification.

任何性能问题排查都先生成binlog:
bash
dotnet build /bl:perf.binlog -m
然后使用
build-perf-diagnostics
技能和binlog工具做系统化的瓶颈识别。

Optimization 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做进一步优化