C#idiomatic C#
spans-and-allocationidioms

Idioms

Spans and allocation

What to do when the profiler says the copies and the collections are the cost: slice without copying, and allocate on purpose.

The garbage collector is fast, and most code should ignore it. When a hot path shows up in a profile, the first fixes are usually the same few: stop copying buffers, stop allocating temporaries per call, and stop paying twice for the same enumeration.

Measure before you optimize allocation

Allocation is the default behaviour of idiomatic C#, and rewriting a cold path around a span buys nothing but harder code. Profile first, then treat only the frames that actually show up.

Corpus: Performance · Fundamentals of garbage collection

Slice with a span instead of copyingCA1846

Span<T> and ReadOnlySpan<T> are a view over existing memory: a slice is a pointer and a length, with no copy and no allocation. A Substring, a ToArray or a LINQ slice allocates the whole piece.

Preferred

csharp
ReadOnlySpan<char> name = line.AsSpan(header.Length);

Avoid

csharp
string name = line.Substring(header.Length);

Corpus: Memory and spans · CA1846: Prefer AsSpan over Substring

stackalloc for a small buffer that never escapes

A stack-allocated buffer costs nothing to reclaim, but it must be small and it must not outlive the method that created it. Keep the size bounded, and never put an unbounded length into it.

Preferred

csharp
Span<byte> header = stackalloc byte[16];

Avoid

csharp
Span<byte> buffer = stackalloc byte[length];   // length comes from the request

Corpus: stackalloc expression

Span for the call, Memory when it has to be stored

Span<T> is a ref struct, so it cannot be a field, a generic argument, or something that survives an await. When a buffer has to cross either boundary, the parameter and the field are Memory<T> and the span is taken at the point of use.

Corpus: Memory<T> usage guidelines

Rent a buffer instead of allocating a large one

ArrayPool<T>.Shared rents an array that the next call can reuse, which keeps a large temporary out of the collector. A rented buffer must be returned in a finally, so every path gives it back.

Preferred

csharp
var buffer = ArrayPool<byte>.Shared.Rent(64 * 1024);
try { await _stream.ReadAsync(buffer.AsMemory(0, 64 * 1024), ct); }
finally { ArrayPool<byte>.Shared.Return(buffer); }

Avoid

csharp
var buffer = new byte[64 * 1024];   // every call, forever

Corpus: Memory and spans

Array.Empty<T>() instead of an empty array literalCA1825

A new T[0] is an allocation for a value that has no state. Array.Empty<T>() returns the same instance every time, which matters most in the code paths that return "nothing" often.

Preferred

csharp
return Array.Empty<string>();

Avoid

csharp
return new string[0];

Corpus: CA1825: Avoid zero-length array allocations

An array created at the call site is created at every callCA1861

A constant array passed as an argument is a real allocation on each call, and in a loop that is one array per iteration. Hoist it to a static readonly field, or use a span over a collection expression.

Preferred

csharp
private static readonly string[] Prefixes = ["api", "cdn"];

static bool IsAsset(string path) =>
    Prefixes.Any(p => path.StartsWith(p, StringComparison.Ordinal));

Avoid

csharp
if (new[] { "api", "cdn" }.Any(p => path.StartsWith(p, StringComparison.Ordinal)))

Corpus: CA1861: Avoid constant arrays as arguments

Use the char overloadsCA1847

Contains(char), IndexOf(char) and StringBuilder.Append(char) avoid the one-character string literal, and the versions that take a StringComparison avoid a culture-aware search where none is wanted.

Preferred

csharp
if (line.Contains(':'))
{
    builder.Append(':');
}

Avoid

csharp
if (line.Contains(":"))
{
    builder.Append(":");
}

Corpus: CA1847: Use string.Contains(char) for single character string searches · CA1834: Use StringBuilder.Append(char) for single character strings

Parse the format string once with CompositeFormatCA1863

string.Format parses its format string on every call, so a format string held in a const or a static field pays that parse again and again. CompositeFormat.Parse does it once, and CA1863 flags the calls that could take the parsed form.

Preferred

csharp
private static readonly CompositeFormat RowFormat = CompositeFormat.Parse("{0}: {1}");

static string WriteRow(Row row) =>
    string.Format(CultureInfo.InvariantCulture, RowFormat, row.Id, row.Name);

Avoid

csharp
private const string RowFormat = "{0}: {1}";

static void WriteRow(Row row) =>
    Console.WriteLine(string.Format(RowFormat, row.Id, row.Name));

Corpus: CA1863: Use CompositeFormat

Pass a large struct by in or ref

A struct bigger than a couple of machine words is copied on every call. in passes it by reference for reading and keeps the call site unchanged; ref is for when the callee writes back.

Preferred

csharp
public double Length(in Matrix4x4 matrix)

Avoid

csharp
public double Length(Matrix4x4 matrix)   // 64 bytes copied per call

Corpus: Ref safety and ref structs

Let an interpolated string handler skip the formatting

An interpolated string is formatted eagerly unless the callee takes an interpolated string handler, which is how ILogger avoids building a message that the log level discards. Pass the interpolation to the logger rather than a formatted string.

Corpus: Interpolated string handlers

Enumerate a sequence onceCA1851

A LINQ query re-runs its work every time it is enumerated, so Any() followed by iteration, or a Count() followed by a loop, does the whole query twice. Materialise it into a list when it is needed more than once.

Preferred

csharp
var items = query.ToList();
if (items.Count > 0)
{
    foreach (var item in items) Use(item);
}

Avoid

csharp
if (query.Any())
{
    foreach (var item in query) Use(item);
}

Corpus: CA1851: Possible multiple enumerations of IEnumerable collection

Jagged arrays are faster than rectangular onesCA1814

A multidimensional array is one object whose index arithmetic the runtime computes, and it has no single-item slice. An array of arrays is faster to walk and each row can be replaced or sliced on its own.

Preferred

csharp
int[][] grid = new int[rows][];

Avoid

csharp
int[,] grid = new int[rows, columns];

Corpus: CA1814: Prefer jagged arrays over multidimensional