dotnet-performance-patterns

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

dotnet-performance-patterns

dotnet-performance-patterns

Performance-oriented architecture patterns for .NET applications. Covers zero-allocation coding with Span<T> and Memory<T>, buffer pooling with ArrayPool<T>, struct design for performance (readonly struct, ref struct, in parameters), sealed class devirtualization by the JIT, stack-based allocation with stackalloc, and string handling performance. Focuses on the why (performance rationale and measurement) rather than the how (language syntax).
Version assumptions: .NET 8.0+ baseline. Span<T> and Memory<T> are available from .NET Core 2.1+ but this skill targets modern usage patterns on .NET 8+.
Out of scope: C# language syntax for Span, records, pattern matching, and collection expressions -- see [skill:dotnet-csharp-modern-patterns]. Coding standards and naming conventions (including sealed class style guidance) -- see [skill:dotnet-csharp-coding-standards]. Microbenchmarking setup and measurement is owned by this epic's companion skill -- see [skill:dotnet-benchmarkdotnet]. Native AOT compilation pipeline and trimming -- see [skill:dotnet-native-aot]. Serialization format performance tradeoffs -- see [skill:dotnet-serialization]. Architecture patterns (caching, resilience, DI) -- see [skill:dotnet-architecture-patterns]. EF Core query optimization -- see [skill:dotnet-efcore-patterns].
Cross-references: [skill:dotnet-benchmarkdotnet] for measuring the impact of these patterns, [skill:dotnet-csharp-modern-patterns] for Span/Memory syntax foundation, [skill:dotnet-csharp-coding-standards] for sealed class style conventions, [skill:dotnet-native-aot] for AOT performance characteristics and trimming impact on pattern choices, [skill:dotnet-serialization] for serialization performance context.

面向.NET应用的性能导向架构模式,涵盖使用Span<T>和Memory<T>的零分配编码、使用ArrayPool<T>的缓冲池、面向性能的struct设计(readonly struct、ref struct、in参数)、JIT对sealed类的去虚拟化、使用stackalloc的栈分配,以及字符串处理性能优化。内容聚焦于为什么这么做(性能原理与量化衡量)而非怎么做(语言语法)。
版本假设: 以.NET 8.0+为基准。Span<T>和Memory<T>从.NET Core 2.1开始可用,但本技能面向.NET 8+的现代化使用模式。
超出范围: Span、记录、模式匹配、集合表达式的C#语言语法——参考[skill:dotnet-csharp-modern-patterns]。编码规范与命名约定(包括sealed类的风格指引)——参考[skill:dotnet-csharp-coding-standards]。微基准测试的搭建与衡量由本体系的配套技能覆盖——参考[skill:dotnet-benchmarkdotnet]。Native AOT编译管道与裁剪——参考[skill:dotnet-native-aot]。序列化格式的性能权衡——参考[skill:dotnet-serialization]。架构模式(缓存、容错、依赖注入)——参考[skill:dotnet-architecture-patterns]。EF Core查询优化——参考[skill:dotnet-efcore-patterns]。
交叉引用:[skill:dotnet-benchmarkdotnet]用于衡量这些模式的优化效果,[skill:dotnet-csharp-modern-patterns]提供Span/Memory的语法基础,[skill:dotnet-csharp-coding-standards]提供sealed类的风格约定,[skill:dotnet-native-aot]提供AOT性能特性与裁剪对模式选择的影响,[skill:dotnet-serialization]提供序列化性能相关上下文。

Span<T> and Memory<T> for Zero-Allocation Scenarios

面向零分配场景的Span<T>与Memory<T>

Why Span<T> Matters for Performance

为什么Span<T>对性能至关重要

Span<T>
provides a safe, bounds-checked view over contiguous memory without allocating. It enables slicing arrays, strings, and stack memory without copying. For syntax details see [skill:dotnet-csharp-modern-patterns]; this section focuses on performance rationale.
Span<T>
提供了对连续内存的安全、带边界检查的视图,无需分配内存。它支持对数组、字符串、栈内存进行切片且无需拷贝。语法细节参考[skill:dotnet-csharp-modern-patterns],本节聚焦于性能原理。

Zero-Allocation String Processing

零分配字符串处理

csharp
// BAD: Substring allocates a new string on each call
public static (string Key, string Value) ParseHeader_Allocating(string header)
{
    var colonIndex = header.IndexOf(':');
    return (header.Substring(0, colonIndex), header.Substring(colonIndex + 1).Trim());
}

// GOOD: ReadOnlySpan<char> slicing avoids all allocations
public static (ReadOnlySpan<char> Key, ReadOnlySpan<char> Value) ParseHeader_ZeroAlloc(
    ReadOnlySpan<char> header)
{
    var colonIndex = header.IndexOf(':');
    return (header[..colonIndex], header[(colonIndex + 1)..].Trim());
}
Performance impact: for high-throughput parsing (HTTP headers, log lines, CSV rows), Span-based parsing eliminates GC pressure entirely. Measure with
[MemoryDiagnoser]
in [skill:dotnet-benchmarkdotnet] -- the
Allocated
column should read
0 B
.
csharp
// 反面示例:每次调用Substring都会分配新的字符串
public static (string Key, string Value) ParseHeader_Allocating(string header)
{
    var colonIndex = header.IndexOf(':');
    return (header.Substring(0, colonIndex), header.Substring(colonIndex + 1).Trim());
}

// 正面示例:ReadOnlySpan<char>切片完全避免内存分配
public static (ReadOnlySpan<char> Key, ReadOnlySpan<char> Value) ParseHeader_ZeroAlloc(
    ReadOnlySpan<char> header)
{
    var colonIndex = header.IndexOf(':');
    return (header[..colonIndex], header[(colonIndex + 1)..].Trim());
}
性能影响:对于高吞吐量解析场景(HTTP头部、日志行、CSV行),基于Span的解析可以完全消除GC压力。使用[skill:dotnet-benchmarkdotnet]的
[MemoryDiagnoser]
进行测量,
Allocated
列应显示为
0 B
。

Memory<T> for Async and Storage Scenarios

面向异步与存储场景的Memory<T>

Span<T>
cannot be used in async methods or stored on the heap (it is a ref struct). Use
Memory<T>
when you need to:
  • Pass buffers to async I/O methods
  • Store a slice reference in a field or collection
  • Return a memory region from a method for later consumption
csharp
public async Task<int> ReadAndProcessAsync(Stream stream, Memory<byte> buffer)
{
    var bytesRead = await stream.ReadAsync(buffer);
    var data = buffer[..bytesRead]; // Memory<T> slicing -- no allocation
    return ProcessData(data.Span);  // .Span for synchronous processing
}

private int ProcessData(ReadOnlySpan<byte> data)
{
    var sum = 0;
    foreach (var b in data)
        sum += b;
    return sum;
}

Span<T>
无法在异步方法中使用,也不能存储在堆上(它是ref struct)。当你需要执行以下操作时请使用
Memory<T>
:
  • 向异步I/O方法传递缓冲区
  • 在字段或集合中存储切片引用
  • 从方法返回内存区域供后续消费
csharp
public async Task<int> ReadAndProcessAsync(Stream stream, Memory<byte> buffer)
{
    var bytesRead = await stream.ReadAsync(buffer);
    var data = buffer[..bytesRead]; // Memory<T>切片——无内存分配
    return ProcessData(data.Span);  // 使用.Span进行同步处理
}

private int ProcessData(ReadOnlySpan<byte> data)
{
    var sum = 0;
    foreach (var b in data)
        sum += b;
    return sum;
}

ArrayPool<T> for Buffer Pooling

用于缓冲池的ArrayPool<T>

Why Pool Buffers

为什么要池化缓冲区

Large array allocations (>= 85,000 bytes) go directly to the Large Object Heap (LOH), which is only collected in Gen 2 GC -- expensive and causes pauses. Even smaller arrays add GC pressure in hot paths.
ArrayPool<T>
rents and returns buffers to avoid repeated allocations.
大型数组分配(>= 85000字节)会直接进入大对象堆(LOH),仅在Gen 2 GC时才会被回收——成本很高且会导致停顿。即使是小型数组在热路径中也会增加GC压力。
ArrayPool<T>
通过租用和归还缓冲区避免重复分配。

Usage Pattern

使用模式

csharp
using System.Buffers;

public int ProcessLargeData(Stream source)
{
    var buffer = ArrayPool<byte>.Shared.Rent(minimumLength: 81920);
    try
    {
        var bytesRead = source.Read(buffer, 0, buffer.Length);
        // IMPORTANT: Rent may return a larger buffer than requested.
        // Always use bytesRead or the requested length, never buffer.Length.
        return ProcessChunk(buffer.AsSpan(0, bytesRead));
    }
    finally
    {
        ArrayPool<byte>.Shared.Return(buffer, clearArray: true);
        // clearArray: true zeroes the buffer -- use when buffer held sensitive data
    }
}
csharp
using System.Buffers;

public int ProcessLargeData(Stream source)
{
    var buffer = ArrayPool<byte>.Shared.Rent(minimumLength: 81920);
    try
    {
        var bytesRead = source.Read(buffer, 0, buffer.Length);
        // 重要提示:Rent可能返回比请求更大的缓冲区
        // 始终使用bytesRead或请求的长度,永远不要用buffer.Length
        return ProcessChunk(buffer.AsSpan(0, bytesRead));
    }
    finally
    {
        ArrayPool<byte>.Shared.Return(buffer, clearArray: true);
        // clearArray: true会清空缓冲区——当缓冲区存储过敏感数据时使用
    }
}

Common Mistakes

常见错误

MistakeImpactFix
Using
buffer.Length
instead of requested size
Processes uninitialized bytes beyond actual dataTrack requested/actual size separately
Forgetting to return the bufferPool exhaustion, falls back to allocationUse try/finally or a
using
wrapper
Returning a buffer twiceCorrupts pool stateNull out the reference after return
Not clearing sensitive dataSecurity leak from pooled buffersPass
clearArray: true
to
Return

错误影响修复方案
使用
buffer.Length
而非请求的大小
会处理超出实际数据的未初始化字节单独跟踪请求/实际数据大小
忘记归还缓冲区池耗尽,回退到直接分配使用try/finally或
using
包装
重复归还同一个缓冲区破坏池的状态归还后将引用置空
未清空敏感数据池化缓冲区导致安全泄露调用
Return
时传
clearArray: true

readonly struct, ref struct, and in Parameters

readonly struct、ref struct与in参数

readonly struct -- Defensive Copy Elimination

readonly struct——消除防御性拷贝

The JIT must defensively copy non-readonly structs when accessed via
in
,
readonly
fields, or
readonly
methods to prevent mutation. Marking a struct
readonly
guarantees immutability, eliminating these copies:
csharp
// GOOD: readonly eliminates defensive copies on every access
public readonly struct Point3D
{
    public double X { get; }
    public double Y { get; }
    public double Z { get; }

    public Point3D(double x, double y, double z) => (X, Y, Z) = (x, y, z);

    // readonly struct: JIT knows this cannot mutate, no defensive copy needed
    public double DistanceTo(in Point3D other)
    {
        var dx = X - other.X;
        var dy = Y - other.Y;
        var dz = Z - other.Z;
        return Math.Sqrt(dx * dx + dy * dy + dz * dz);
    }
}
Without
readonly
, calling a method on a struct through an
in
parameter forces the JIT to copy the entire struct to protect against mutation. For large structs in tight loops, this eliminates significant overhead.
当通过
in
、
readonly
字段或
readonly
方法访问非readonly struct时,JIT必须对其进行防御性拷贝以避免修改。将struct标记为
readonly
可以保证不可变性,消除这些拷贝:
csharp
// 正面示例:readonly消除每次访问时的防御性拷贝
public readonly struct Point3D
{
    public double X { get; }
    public double Y { get; }
    public double Z { get; }

    public Point3D(double x, double y, double z) => (X, Y, Z) = (x, y, z);

    // readonly struct:JIT知道不会发生修改,无需防御性拷贝
    public double DistanceTo(in Point3D other)
    {
        var dx = X - other.X;
        var dy = Y - other.Y;
        var dz = Z - other.Z;
        return Math.Sqrt(dx * dx + dy * dy + dz * dz);
    }
}
如果没有
readonly
,通过
in
参数调用struct上的方法会强制JIT拷贝整个struct以防止修改。对于紧循环中的大型struct,这可以消除大量开销。

ref struct -- Stack-Only Types

ref struct——仅栈类型

ref struct
types are constrained to the stack. They cannot be boxed, stored in fields, or used in async methods. This enables safe wrapping of Span<T>:
csharp
public ref struct SpanLineEnumerator
{
    private ReadOnlySpan<char> _remaining;

    public SpanLineEnumerator(ReadOnlySpan<char> text) => _remaining = text;

    public ReadOnlySpan<char> Current { get; private set; }

    public bool MoveNext()
    {
        if (_remaining.IsEmpty)
            return false;

        var newlineIndex = _remaining.IndexOf('\n');
        if (newlineIndex == -1)
        {
            Current = _remaining;
            _remaining = default;
        }
        else
        {
            Current = _remaining[..newlineIndex];
            _remaining = _remaining[(newlineIndex + 1)..];
        }
        return true;
    }
}
ref struct
类型被限制只能在栈上使用,不能被装箱、存储在字段中,也不能在异步方法中使用。这使得它可以安全地包装Span<T>:
csharp
public ref struct SpanLineEnumerator
{
    private ReadOnlySpan<char> _remaining;

    public SpanLineEnumerator(ReadOnlySpan<char> text) => _remaining = text;

    public ReadOnlySpan<char> Current { get; private set; }

    public bool MoveNext()
    {
        if (_remaining.IsEmpty)
            return false;

        var newlineIndex = _remaining.IndexOf('\n');
        if (newlineIndex == -1)
        {
            Current = _remaining;
            _remaining = default;
        }
        else
        {
            Current = _remaining[..newlineIndex];
            _remaining = _remaining[(newlineIndex + 1)..];
        }
        return true;
    }
}

in Parameters -- Pass-by-Reference Without Mutation

in参数——不可变的按引用传递

Use
in
for large readonly structs passed to methods. The
in
modifier passes by reference (avoids copying) and prevents mutation:
csharp
// in parameter: pass by reference, no copy, no mutation allowed
public static double CalculateDistance(in Point3D a, in Point3D b)
    => a.DistanceTo(in b);
When to use
in
:
Struct SizeRecommendation
<= 16 bytesPass by value (register-friendly, no indirection overhead)
> 16 bytesUse
in
to avoid copy overhead
Any size, readonly struct
in
is safe (no defensive copies)
Any size, non-readonly structAvoid
in
(defensive copies negate the benefit)

对于传递给方法的大型readonly struct请使用
in
。
in
修饰符实现按引用传递(避免拷贝)同时防止修改:
csharp
// in参数:按引用传递,无拷贝,不允许修改
public static double CalculateDistance(in Point3D a, in Point3D b)
    => a.DistanceTo(in b);
何时使用
in
:
Struct大小建议
<= 16字节按值传递(对寄存器友好,无间接访问开销)
> 16字节使用
in
避免拷贝开销
任意大小的readonly struct使用
in
是安全的(无防御性拷贝)
任意大小的非readonly struct避免使用
in
(防御性拷贝会抵消收益)

Sealed Class Performance Rationale

Sealed类的性能原理

JIT Devirtualization

JIT去虚拟化

When a class is
sealed
, the JIT can replace virtual method calls with direct calls (devirtualization) because no subclass override is possible. This enables further inlining:
csharp
// Without sealed: virtual dispatch through vtable
public class OpenService : IProcessor
{
    public virtual int Process(int x) => x * 2;
}

// With sealed: JIT devirtualizes + inlines Process call
public sealed class SealedService : IProcessor
{
    public int Process(int x) => x * 2;
}

public interface IProcessor { int Process(int x); }
Verify devirtualization with
[DisassemblyDiagnoser]
in [skill:dotnet-benchmarkdotnet]. See [skill:dotnet-csharp-coding-standards] for the project convention of defaulting to sealed classes.
当类被标记为
sealed
时,JIT可以将虚方法调用替换为直接调用(去虚拟化),因为不存在子类重写的可能。这进一步支持了方法内联:
csharp
// 未加sealed:通过vtable进行虚派发
public class OpenService : IProcessor
{
    public virtual int Process(int x) => x * 2;
}

// 加了sealed:JIT去虚拟化+内联Process调用
public sealed class SealedService : IProcessor
{
    public int Process(int x) => x * 2;
}

public interface IProcessor { int Process(int x); }
使用[skill:dotnet-benchmarkdotnet]的
[DisassemblyDiagnoser]
验证去虚拟化效果。项目中默认使用sealed类的约定参考[skill:dotnet-csharp-coding-standards]。

Performance Impact

性能影响

Devirtualization + inlining eliminates:
  1. vtable lookup -- indirect memory access to find the method pointer
  2. Call overhead -- the actual indirect call instruction
  3. Inlining barrier -- virtual calls cannot be inlined; sealed methods can
In tight loops and hot paths, the cumulative effect is measurable. For framework/library types that are not designed for extension, always prefer
sealed
.

去虚拟化+内联消除了:
  1. vtable查找——查找方法指针的间接内存访问
  2. 调用开销——实际的间接调用指令
  3. 内联屏障——虚调用无法被内联,sealed方法可以
在紧循环和热路径中,累计效果是可测量的。对于不设计为可扩展的框架/库类型,始终优先使用
sealed
。

stackalloc for Small Stack-Based Allocations

用于小型栈分配的stackalloc

When to Use stackalloc

何时使用stackalloc

stackalloc
allocates memory on the stack, avoiding GC entirely. Use for small, fixed-size buffers in hot paths:
csharp
public static string FormatGuid(Guid guid)
{
    // 68 bytes on the stack -- well within safe limits
    Span<char> buffer = stackalloc char[68];
    guid.TryFormat(buffer, out var charsWritten, "D");
    return new string(buffer[..charsWritten]);
}
stackalloc
在栈上分配内存,完全避免GC。适用于热路径中的小型固定大小缓冲区:
csharp
public static string FormatGuid(Guid guid)
{
    // 栈上分配68字节——完全在安全范围内
    Span<char> buffer = stackalloc char[68];
    guid.TryFormat(buffer, out var charsWritten, "D");
    return new string(buffer[..charsWritten]);
}

Safety Guidelines

安全指引

GuidelineRationale
Keep allocations small (< 1 KB typical, < 4 KB absolute maximum)Stack space is limited (~1 MB default on Windows); overflow crashes the process
Use constant or bounded sizes onlyRuntime-variable sizes risk stack overflow from malicious/unexpected input
Prefer
Span<T>
assignment over raw pointer
Span provides bounds checking; raw pointers do not
Fall back to ArrayPool for large/variable sizesGracefully handle cases that exceed stack budget
指引原理
保持分配量较小(通常<1KB,绝对最大值<4KB)栈空间有限(Windows默认约1MB);栈溢出会直接崩溃进程
仅使用常量或有界大小运行时可变大小可能因恶意/意外输入导致栈溢出
优先使用
Span<T>
赋值而非原始指针
Span提供边界检查,原始指针没有
大型/可变大小回退到ArrayPool优雅处理超出栈预算的场景

Hybrid Pattern: stackalloc with ArrayPool Fallback

混合模式:stackalloc加ArrayPool回退

csharp
public static string ProcessData(ReadOnlySpan<byte> input)
{
    const int stackThreshold = 256;
    char[]? rented = null;

    Span<char> buffer = input.Length <= stackThreshold
        ? stackalloc char[stackThreshold]
        : (rented = ArrayPool<char>.Shared.Rent(input.Length));

    try
    {
        var written = Encoding.UTF8.GetChars(input, buffer);
        return new string(buffer[..written]);
    }
    finally
    {
        if (rented is not null)
            ArrayPool<char>.Shared.Return(rented);
    }
}
This pattern is used throughout the .NET runtime libraries and is the recommended approach for methods that handle both small and large inputs.

csharp
public static string ProcessData(ReadOnlySpan<byte> input)
{
    const int stackThreshold = 256;
    char[]? rented = null;

    Span<char> buffer = input.Length <= stackThreshold
        ? stackalloc char[stackThreshold]
        : (rented = ArrayPool<char>.Shared.Rent(input.Length));

    try
    {
        var written = Encoding.UTF8.GetChars(input, buffer);
        return new string(buffer[..written]);
    }
    finally
    {
        if (rented is not null)
            ArrayPool<char>.Shared.Return(rented);
    }
}
这种模式在.NET运行时库中被广泛使用,是处理大小不一输入的推荐方案。

String Interning and StringComparison Performance

字符串驻留与StringComparison性能

String Comparison Performance

字符串比较性能

Ordinal comparisons are significantly faster than culture-aware comparisons because they avoid Unicode normalization:
csharp
// FAST: ordinal comparison (byte-by-byte)
bool isMatch = str.Equals("expected", StringComparison.Ordinal);
bool containsKey = dict.ContainsKey(key); // Dictionary<string, T> uses ordinal by default

// FAST: case-insensitive ordinal (no culture overhead)
bool isMatchIgnoreCase = str.Equals("expected", StringComparison.OrdinalIgnoreCase);

// SLOW: culture-aware comparison (Unicode normalization, linguistic rules)
bool isMatchCulture = str.Equals("expected", StringComparison.CurrentCulture);
Default guidance: Use
StringComparison.Ordinal
or
StringComparison.OrdinalIgnoreCase
for internal identifiers, dictionary keys, file paths, and protocol strings. Reserve culture-aware comparison for user-visible text sorting and display.
序号比较比文化感知比较快得多,因为它们避免了Unicode规范化:
csharp
// 快:序号比较(逐字节对比)
bool isMatch = str.Equals("expected", StringComparison.Ordinal);
bool containsKey = dict.ContainsKey(key); // Dictionary<string, T>默认使用序号比较

// 快:不区分大小写的序号比较(无文化开销)
bool isMatchIgnoreCase = str.Equals("expected", StringComparison.OrdinalIgnoreCase);

// 慢:文化感知比较(Unicode规范化、语言学规则)
bool isMatchCulture = str.Equals("expected", StringComparison.CurrentCulture);
默认指引: 内部标识符、字典键、文件路径、协议字符串使用
StringComparison.Ordinal
或
StringComparison.OrdinalIgnoreCase
。文化感知比较仅用于用户可见文本的排序和展示。

String Interning

字符串驻留

The CLR interns compile-time string literals automatically.
string.Intern()
can reduce memory for runtime strings that repeat frequently:
csharp
// Intern frequently-repeated runtime strings to share a single instance
var normalized = string.Intern(headerName.ToLowerInvariant());
Caution: Interned strings are never garbage collected. Only intern strings from a bounded, known set (HTTP headers, XML element names). Never intern user input or unbounded data.
CLR会自动驻留编译时字符串字面量。
string.Intern()
可以减少高频重复运行时字符串的内存占用:
csharp
// 驻留高频重复的运行时字符串以共享单实例
var normalized = string.Intern(headerName.ToLowerInvariant());
注意: 驻留的字符串永远不会被GC回收。仅驻留自有界已知集合的字符串(HTTP头部、XML元素名),永远不要驻留用户输入或无界数据。

Efficient String Building

高效字符串构建

ScenarioRecommended ApproachWhy
2-3 concatenationsString interpolation
$"{a}{b}"
Compiler optimizes to
string.Concat
Loop concatenation
StringBuilder
Avoids quadratic allocation
Known fixed parts
string.Create
Single allocation, Span-based writing
High-throughput formatting
Span<char>
+
TryFormat
Zero-allocation formatting
csharp
// string.Create for single-allocation building
public static string FormatId(int category, int item)
{
    return string.Create(11, (category, item), static (span, state) =>
    {
        state.category.TryFormat(span, out var catWritten);
        span[catWritten] = '-';
        state.item.TryFormat(span[(catWritten + 1)..], out _);
    });
}

场景推荐方案原因
2-3次拼接字符串插值
$"{a}{b}"
编译器优化为
string.Concat
循环拼接
StringBuilder
避免二次方分配
固定部分已知
string.Create
单次分配,基于Span写入
高吞吐量格式化
Span<char>
+
TryFormat
零分配格式化
csharp
// string.Create实现单次分配构建
public static string FormatId(int category, int item)
{
    return string.Create(11, (category, item), static (span, state) =>
    {
        state.category.TryFormat(span, out var catWritten);
        span[catWritten] = '-';
        state.item.TryFormat(span[(catWritten + 1)..], out _);
    });
}

Performance Measurement Checklist

性能衡量检查清单

Before applying any optimization pattern, measure first. Premature optimization without data leads to complex code with no measurable benefit.
  1. Identify the hot path -- use [skill:dotnet-benchmarkdotnet] to establish a baseline
  2. Measure allocations -- enable
    [MemoryDiagnoser]
    and check the
    Allocated
    column
  3. Apply one pattern at a time -- change one thing, re-measure, compare to baseline
  4. Check AOT impact -- if targeting Native AOT ([skill:dotnet-native-aot]), verify patterns are trim-safe
  5. Verify with production-like data -- synthetic benchmarks can miss real-world allocation patterns
  6. Document the tradeoff -- every optimization trades readability or flexibility for speed; record the measured gain

应用任何优化模式前请先测量。没有数据的过早优化会导致代码复杂但没有可测量的收益。
  1. 识别热路径——使用[skill:dotnet-benchmarkdotnet]建立基准线
  2. 测量分配量——启用
    [MemoryDiagnoser]
    检查
    Allocated
    列
  3. 每次应用一种模式——只改一个点,重新测量,与基准线对比
  4. 检查AOT影响——如果目标是Native AOT([skill:dotnet-native-aot]),验证模式是裁剪安全的
  5. 使用类生产数据验证——合成基准测试可能会遗漏真实世界的分配模式
  6. 记录权衡——每个优化都是用可读性或灵活性换速度,记录测量到的收益

Agent Gotchas

Agent注意事项

  1. Measure before optimizing -- never apply Span/ArrayPool/stackalloc without a benchmark showing the allocation or latency problem. Premature optimization produces unreadable code for no measurable gain.
  2. Do not use stackalloc with variable sizes from untrusted input -- stack overflow crashes the process with no exception handler. Always validate bounds or use the hybrid stackalloc/ArrayPool pattern.
  3. Always mark value types
    readonly struct
    when they are immutable
    -- without
    readonly
    , the JIT generates defensive copies on every
    in
    parameter access and
    readonly
    field access, silently negating the performance benefit of using structs.
  4. Return rented ArrayPool buffers in finally blocks -- forgetting to return starves the pool and causes fallback allocations that negate the benefit.
  5. Use
    StringComparison.Ordinal
    for internal comparisons
    -- omitting the comparison parameter defaults to culture-aware comparison, which is slower and produces surprising results for technical strings (file paths, identifiers).
  6. Sealed classes help performance only when the JIT can see the concrete type -- if the object is accessed through an interface variable in a non-devirtualizable call site, sealing provides no benefit. Verify with
    [DisassemblyDiagnoser]
    .
  7. Do not re-teach language syntax -- reference [skill:dotnet-csharp-modern-patterns] for Span/Memory syntax details. This skill focuses on when and why to use these patterns for performance.

  1. 优化前先测量——不要在没有基准测试证明分配或延迟问题的情况下应用Span/ArrayPool/stackalloc。过早优化会产生难以阅读的代码,且没有可测量的收益。
  2. 不要对来自不可信输入的可变大小使用stackalloc——栈溢出会直接崩溃进程,没有异常处理机会。始终验证边界,或使用stackalloc/ArrayPool混合模式。
  3. 不可变值类型始终标记为
    readonly struct
    ——如果没有
    readonly
    ,JIT会在每次
    in
    参数访问和
    readonly
    字段访问时生成防御性拷贝,悄无声息地抵消使用struct的性能收益。
  4. 在finally块中归还租用的ArrayPool缓冲区——忘记归还会导致池饥饿,触发回退分配,抵消优化收益。
  5. 内部比较使用
    StringComparison.Ordinal
    ——省略比较参数会默认使用文化感知比较,速度更慢,且对于技术字符串(文件路径、标识符)会产生意外结果。
  6. 仅当JIT可以看到具体类型时sealed类才会提升性能——如果对象是通过接口变量在无法去虚拟化的调用点访问,密封没有收益。使用
    [DisassemblyDiagnoser]
    验证。
  7. 不要重复讲授语言语法——Span/Memory语法细节参考[skill:dotnet-csharp-modern-patterns]。本技能聚焦于何时以及为什么要使用这些模式来提升性能。

Knowledge Sources

知识来源

Performance patterns in this skill are grounded in guidance from:
  • Stephen Toub -- .NET Performance blog series (devblogs.microsoft.com/dotnet/author/toub). Authoritative source on Span<T>, ValueTask, ArrayPool, async internals, and runtime performance characteristics.
  • Stephen Cleary -- Async best practices and concurrent collections guidance. Author of Concurrency in C# Cookbook.
  • Nick Chapsas -- Modern .NET performance patterns and benchmarking methodology.
These sources inform the patterns and rationale presented above. This skill does not claim to represent or speak for any individual.
本技能中的性能模式基于以下来源的指引:
  • Stephen Toub——.NET性能博客系列(devblogs.microsoft.com/dotnet/author/toub)。Span<T>、ValueTask、ArrayPool、异步内部实现、运行时性能特性的权威来源。
  • Stephen Cleary——异步最佳实践与并发集合指引,《Concurrency in C# Cookbook》作者。
  • Nick Chapsas——现代化.NET性能模式与基准测试方法论。
以上来源为本文呈现的模式和原理提供了参考,本技能不代表任何个人观点。