XPerf
LanguagesC#

C# performance guide (v0)

Fewer reallocating lists, quieter hot paths.

Scope: modern C# (.NET). Speedup claims cite a local bench, an upstream source, or are marked provisional. Corpus residual counts are lint hit counts, not wall-time savings.

Profile first (BenchmarkDotNet, dotnet-trace). Runtime and GC settings differ — re-bench.

Contents

1. Lists and strings

List.Add in a loop without setting Capacity reallocates. Prefer sizing once. String += in a tight loop allocates; prefer StringBuilder.

Calling static Regex.IsMatch / Regex.Match with a pattern string on every iteration looks the pattern up in the static regex cache each time; prefer one reused Regex instance (RegexOptions.Compiled when the pattern is hot).

Lint: cs-list-add-no-capacity, cs-string-plus-hot (default off); cs-regex-hot (default hint).

2. Hot I/O

Console.Write / WriteLine in a tight loop is expensive. Gate debug output or buffer.

Lint: cs-console-hot (default off).

3. Materialize and format

ToList / ToArray / ToDictionary inside a loop allocates a fresh collection every iteration. string.Format inside a loop allocates a new string every iteration. JsonSerializer.Serialize / Deserialize and JsonConvert.SerializeObject / DeserializeObject inside a loop encode or decode on every iteration. cs-json-hot stays hint. The microbench is a fixed payload. It is not part of the 2026-10-22 corpus-ready verdict.

File.ReadAllText of one path was about 338× on .NET 10 versus reading once. Stopwatch.GetTimestamp was about 37× versus one sample. new Regex every iteration, with both sides still matching, was about 7× and is the same hint as the static Regex.IsMatch rule. A changing path cannot be hoisted, and a loop that needs a fresh timestamp has nothing to hoist.

GetField plus GetValue every iteration was about 62× versus reading the field. Holding the FieldInfo and still calling GetValue was about 42× versus the field, so moving only GetField outside is a small change. A name that changes every iteration cannot be turned into a direct field. This is the same family as Go FieldByName.

Lint: cs-linq-materialize-hot, cs-format-hot (default hint; pending measure); cs-json-hot, cs-regex-hot, cs-file-read-hot, cs-clock-hot, cs-field-by-name-hot (default hint).

Lint map

IDSmellDefault
cs-list-add-no-capacityList.Add in loop without Capacityoff
cs-string-plus-hotstring += in loopoff
cs-console-hotConsole.Write* in loopoff
cs-regex-hotstatic Regex.IsMatch/Match in loophint
cs-linq-materialize-hotToList / ToArray / ToDictionary in loophint (pending measure)
cs-format-hotstring.Format in loophint (pending measure)
cs-json-hotJSON encode/decode in loophint (JSONL, fixed payload only)

Escalate only with evidence/<rule>.jsonl — see evidence-policy.md.

References