Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Guides performance optimization for MSBuild engine code. Consult when working on hot paths in evaluation or execution, reducing allocations, choosing collection types, handling strings efficiently, modifying Expander.cs or Evaluator.cs, using Span<T>/stackalloc, caching values, or profiling build performance. Also applies when reviewing PRs for performance regression.
.claude/skills/dotnet-optimizing-msbuild-performance/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-02 | ✗→✓ | ▲ Improved | -36% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 5% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 53% | 0% |
| case-04 | ✓→✓ | = Same ✓ | -8% | 0% |
| case-05 | ✓→✓ | = Same ✓ | -5% | 0% |
MSBuild evaluates and builds thousands of projects in enterprise solutions. Performance is an architectural concern, not an afterthought.
Profile before optimizing; measure, do not guess. Use BenchmarkDotNet, the evaluation profiler (/profileevaluation), or ETW traces to identify actual bottlenecks before optimizing.
See evaluation-profiling.md and General_perf_onepager.md for profiling tools.
The evaluation and execution engines process millions of operations per build. Unnecessary allocations cause GC pressure that compounds across large solutions.
Where, Select, Any, First all allocate enumerator objects and delegate closures. Use foreach loops instead.csharp // BAD — allocates iterator + delegate var match = items.FirstOrDefault(i => i.Name == name);
// GOOD — zero allocations foreach (var item in items) { if (item.Name == name) { / found / break; } }
string.Concat, interpolation with Span<T>, or StringBuilder reuse — not string.Format on hot paths.Span<T> and stackalloc for short-lived buffers when parsing or slicing strings. Avoid Substring when you only need to compare or inspect a portion.Lazy<T>.MSBuild property, item, and target names are case-insensitive. Getting this wrong causes subtle bugs and perf issues.
| Scenario | Use | |----------|-----| | Property/item/target names | MSBuildNameIgnoreCaseComparer | | General MSBuild identifiers | StringComparison.OrdinalIgnoreCase | | Dictionary keys for MSBuild names | MSBuildNameIgnoreCaseComparer as comparer | | File paths on Windows | StringComparison.OrdinalIgnoreCase | | File paths on Linux | StringComparison.Ordinal |
Never use ToLower()/ToUpper() for comparisons — they allocate a new string every time. Never use CurrentCulture for MSBuild identifiers — build behavior must not vary by locale.
| Access Pattern | Recommended Type | |---------------|-----------------| | Small fixed set (< ~8 items) | Array or ReadOnlySpan<T> | | Build-once, read-many | ImmutableArray<T> (not ImmutableList<T>) | | Keyed lookup, many items | Dictionary<TKey, TValue> with appropriate comparer | | Keyed lookup, few items (< 5) | Linear scan over array (cache-friendly, avoids dict overhead) | | Concurrent reads, rare writes | ImmutableDictionary or snapshot pattern | | Ordered iteration needed | List<T> or array; avoid HashSet<T> if order matters |
ImmutableList<T> is almost never the right choice — it has O(log n) access vs O(1) for ImmutableArray<T>.
These areas are performance-critical and require extra scrutiny:
Expander.cs — property/item/metadata expansion during evaluationEvaluator.cs — project evaluation orchestrationItemSpec.cs / LazyItemEvaluator.cs — item evaluation and globbingTaskExecutionHost.cs — task parameter marshalingFileMatcher.cs, glob operations, project loadingFor extremely hot methods, consider:
[MethodImpl(MethodImplOptions.AggressiveInlining)] for small methods called millions of timesstruct enumerators to avoid heap allocation**/*) are expensive.$([System.IO.Path]::...)) are interpreted and slower than built-in operations.| Anti-Pattern | Why It's Bad | Fix | |-------------|-------------|-----| | items.Count() > 0 | Enumerates entire collection | items.Any() or check .Count property | | string.Format in log messages at Low importance | Allocates even if message is filtered | Use structured logging or guard with verbosity check | | new List<T>(enumerable).ToArray() | Double allocation | enumerable.ToArray() directly | | dict.ContainsKey(k) then dict[k] | Double lookup | dict.TryGetValue(k, out var v) | | Regex in a loop without RegexOptions.Compiled | Reinterprets pattern each time | Compile or use static Regex field |
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→fail | 24,143 | 35,763 | +48% | 1 | 1 | 0% | 4,072 | 5,066 | +24% | 0 | 0 | — |
case-02 | fail→pass | 36,193 | 14,655 | -60% | 1 | 1 | 0% | 5,931 | 3,799 | -36% | 0 | 0 | — |
case-03 | fail→pass | 17,160 | 10,549 | -39% | 1 | 1 | 0% | 2,864 | 2,997 | +5% | 0 | 0 | — |
case-04 | pass→pass | 14,034 | 5,508 | -61% | 1 | 1 | 0% | 2,333 | 2,142 | -8% | 0 | 0 | — |
case-05 | pass→pass | 16,422 | 7,052 | -57% | 1 | 1 | 0% | 2,489 | 2,366 | -5% | 0 | 0 | — |
case-11 | pass→pass | 9,098 | 6,717 | -26% | 1 | 1 | 0% | 1,684 | 2,341 | +39% | 0 | 0 | — |
case-06 | pass→pass | 19,038 | 14,185 | -25% | 1 | 1 | 0% | 3,192 | 3,116 | -2% | 0 | 0 | — |
case-07 | fail→pass | 9,478 | 7,170 | -24% | 1 | 1 | 0% | 1,482 | 2,271 | +53% | 0 | 0 | — |
case-08 | pass→pass | 21,891 | 9,503 | -57% | 1 | 1 | 0% | 3,742 | 2,899 | -23% | 0 | 0 | — |
case-09 | pass→pass | 7,616 | 5,134 | -33% | 1 | 1 | 0% | 1,373 | 2,048 | +49% | 0 | 0 | — |
case-10 | pass→pass | 14,699 | 12,885 | -12% | 1 | 1 | 0% | 2,708 | 3,626 | +34% | 0 | 0 | — |
case-12 | pass→pass | 17,043 | 9,331 | -45% | 1 | 1 | 0% | 2,835 | 2,649 | -7% | 0 | 0 | — |
case-13 | pass→pass | 10,182 | 6,458 | -37% | 1 | 1 | 0% | 1,551 | 2,227 | +44% | 0 | 0 | — |
case-14 | fail→fail | 17,175 | 12,442 | -28% | 1 | 1 | 0% | 3,052 | 3,287 | +8% | 0 | 0 | — |
case-15 | pass→pass | 13,038 | 6,132 | -53% | 1 | 1 | 0% | 2,320 | 2,154 | -7% | 0 | 0 | — |
case-16 | pass→pass | 10,431 | 10,789 | +3% | 1 | 1 | 0% | 1,750 | 2,985 | +71% | 0 | 0 | — |
case-17 | pass→pass | 16,600 | 11,284 | -32% | 1 | 1 | 0% | 2,509 | 3,010 | +20% | 0 | 0 | — |
case-18 | pass→pass | 9,120 | 4,838 | -47% | 1 | 1 | 0% | 1,279 | 2,009 | +57% | 0 | 0 | — |
case-19 | pass→pass | 17,452 | 8,068 | -54% | 1 | 1 | 0% | 2,740 | 2,587 | -6% | 0 | 0 | — |
case-20 | pass→pass | 17,661 | 8,502 | -52% | 1 | 1 | 0% | 2,790 | 2,401 | -14% | 0 | 0 | — |
case-21 | pass→pass | 14,699 | 8,482 | -42% | 1 | 1 | 0% | 2,531 | 2,564 | +1% | 0 | 0 | — |
case-22 | pass→pass | 14,368 | 14,807 | +3% | 1 | 1 | 0% | 2,785 | 3,922 | +41% | 0 | 0 | — |
case-23 | pass→pass | 13,897 | 11,903 | -14% | 1 | 1 | 0% | 2,853 | 3,432 | +20% | 0 | 0 | — |
case-24 | pass→pass | 16,273 | 12,732 | -22% | 1 | 1 | 0% | 2,968 | 3,596 | +21% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 24 cases were attempted. The headline lift of +13 percentage points is the difference between those two pass rates over the 24 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.