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.
74
93%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
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.
Avoid LINQ in hot paths. Where, Select, Any, First all allocate enumerator objects and delegate closures. Use foreach loops instead.
// 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; }
}Avoid string allocations in formatting. Use string.Concat, interpolation with Span<T>, or StringBuilder reuse — not string.Format on hot paths.
Prefer 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.
Cache computed values. If a value is computed in a loop but doesn't change, hoist it out. If it's expensive and reused across calls, use a field or 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 |
c88db8e
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.