C# 高级教程 - 20 性能优化与诊断

性能优化不是”过早优化”,而是基于数据的科学工程实践。.NET 生态提供了世界一流的诊断工具链——从 BenchmarkDotNet 到 OpenTelemetry,从 dotnet-trace 到 PerfView。本文将系统性地介绍性能分析、诊断工具和优化策略。

性能分析基础

基准测试(BenchmarkDotNet)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 安装: dotnet add package BenchmarkDotNet

using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

[MemoryDiagnoser] // 报告内存分配
[DisassemblyDiagnoser] // 查看 JIT 生成的汇编
[RankColumn] // 排名
public class StringBenchmarks
{
private readonly string _text = "hello world";
private readonly int _iterations = 1000;

[Benchmark(Baseline = true)]
public bool ContainsChar()
{
return _text.Contains('o');
}

[Benchmark]
public bool IndexOfChar()
{
return _text.IndexOf('o') >= 0;
}

[Benchmark]
public bool ManualLoop()
{
for (int i = 0; i < _text.Length; i++)
if (_text[i] == 'o') return true;
return false;
}
}

// 运行
BenchmarkRunner.Run<StringBenchmarks>();

性能计数器(dotnet-counters)

1
2
3
4
5
6
# 安装
dotnet tool install --global dotnet-counters

# 监视进程(刷新率 1 秒)
dotnet-counters monitor --process-id 1234 --refresh-interval 1 \
System.Runtime[cpu-usage,gc-heap-size,time-in-gc]

跟踪(dotnet-trace)

1
2
3
4
5
6
7
# 安装
dotnet tool install --global dotnet-trace

# 收集 30 秒 CPU 采样(生成 .nettrace 文件)
dotnet-trace collect --process-id 1234 --profile gc-verbose --duration 00:00:30

# 在 Visual Studio / PerfView 中打开分析

内存优化策略

减少分配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 1. 避免 LINQ 中不必要的分配
// 差
var filtered = list.Where(x => x > 10).ToList();

// 好 —— 使用列表初始化容量
var result = new List<int>(list.Count);
foreach (var x in list)
if (x > 10) result.Add(x);

// 2. 使用 StringBuilder 避免字符串拼接
// 差
string s = "";
for (int i = 0; i < 1000; i++)
s += i; // 每次创建新字符串

// 好
var sb = new StringBuilder();
for (int i = 0; i < 1000; i++)
sb.Append(i);

// 3. 缓存重复对象
// 差
decimal discounts = products.Sum(p => CalculateDiscount(p, GetCurrentDate()));
dozen more uses of GetCurrentDate()... // 重复调用

// 好
var today = GetCurrentDate();
decimal discounts = products.Sum(p => CalculateDiscount(p, today));

// 4. 使用 ArrayPool 减少 GC 压力
var buffer = ArrayPool<byte>.Shared.Rent(4096);
try { Process(buffer); }
finally { ArrayPool<byte>.Shared.Return(buffer); }

结构体优化

1
2
3
4
5
6
7
8
9
10
11
12
// 大结构体(> 16 字节)按引用传递
// 差
public void ProcessPoint(Point p) { /* 复制 24 字节 */ }

// 好
public void ProcessPoint(in Point p) { /* 只传引用 */ }

// 避免结构体装箱
int x = ((IComparable)42).CompareTo(10); // 装箱

// 使用泛型约束避免装箱
public void Compare<T>(T a, T b) where T : IComparable<T> { }

对象池

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
using Microsoft.Extensions.ObjectPool;

public class MyPooledObject
{
public int Data { get; set; }
public void Reset() => Data = 0;
}

var pool = new DefaultObjectPool<MyPooledObject>(
new DefaultPooledObjectPolicy<MyPooledObject>());

// 获取
var obj = pool.Get();
try
{
obj.Data = 42;
// 使用
}
finally
{
pool.Return(obj); // 自动调用 Reset
}

CPU 优化

热路径优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 1. 使用本地引用避免重复属性访问
// 差
for (int i = 0; i < collection.Items.Count; i++)
{
Process(collection.Items[i]); // 每次访问 .Items 属性
}

// 好
var items = collection.Items;
for (int i = 0; i < items.Count; i++)
Process(items[i]);

// 2. 使用 switch 表达式代替字典
static string GetName(int id) => id switch
{
1 => "One",
2 => "Two",
3 => "Three",
_ => "Unknown"
};

// 3. 避免闭包分配
// 差
var threshold = 50;
var filtered = list.Where(x => x > threshold); // 闭包捕获 threshold

// 好
var filtered = list.Where(x => x > 50); // 静态方法,零分配

SIMD 向量化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
using System.Numerics;

// 计算数组和(自动向量化)
static float SumVectorized(float[] values)
{
var sum = Vector<float>.Zero;
int i = 0;

// 一次处理 Vector<float>.Count 个元素
for (; i <= values.Length - Vector<float>.Count; i += Vector<float>.Count)
{
sum += new Vector<float>(values, i);
}

float total = Vector.Dot(sum, Vector<float>.One); // 归约

// 处理剩余元素
for (; i < values.Length; i++)
total += values[i];

return total;
}

字符串优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 使用 SearchValues(.NET 8+)
using System.Buffers;

private static readonly SearchValues<char> _separators =
SearchValues.Create([',', ';', '|']);

bool IsSeparator(char c) => _separators.Contains(c);

// 字符串拆分(零分配)
ReadOnlySpan<char> span = "a,b,c";
while (span.Length > 0)
{
int index = span.IndexOfAny(_separators);
var segment = index < 0 ? span : span[..index];
Console.WriteLine(segment.ToString()); // 仅在需要字符串时分配
span = index < 0 ? [] : span[(index + 1)..];
}

异步优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 1. 避免 async Task 包装同步方法
// 差
public async Task<int> GetValueAsync()
{
return await Task.Run(() => Compute()); // 不必要的线程切换
}

// 好
public int GetValueSync() => Compute();

// 2. 使用 ValueTask 避免分配
public ValueTask<int> GetCachedAsync()
{
if (_cached.HasValue)
return new ValueTask<int>(_cached.Value);
return new ValueTask<int>(FetchAsync());
}

// 3. 使用 ConfigureAwait(false)(库代码)
await Task.Delay(100).ConfigureAwait(false);

诊断与日志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 使用 OpenTelemetry 分布式追踪
using OpenTelemetry;
using OpenTelemetry.Trace;

using var tracerProvider = Sdk.CreateTracerProviderBuilder()
.AddSource("MyApp")
.AddConsoleExporter()
.AddJaegerExporter()
.Build();

using var activity = MyActivitySource.StartActivity("ProcessOrder");
activity?.SetTag("order.id", order.Id);
activity?.AddEvent(new ActivityEvent("开始处理"));

// 结构化日志(Microsoft.Extensions.Logging)
using Microsoft.Extensions.Logging;

var logger = loggerFactory.CreateLogger<OrderService>();
logger.LogInformation("处理订单 {OrderId},金额 {Amount}", order.Id, order.Amount);
// 非字符串拼接 —— 参数化日志,支持结构化输出

.NET 性能清单

分类 优化项 预期收益
内存 使用 ArrayPool/对象池 减少 GC 压力 50-80%
内存 值类型代替引用类型 减少堆分配,提升缓存友好性
内存 使用 Span/Memory 零分配切片
CPU SIMD 向量化 数据并行加速 2-8x
CPU 使用 SearchValues 字符搜索加速 3-5x
异步 ValueTask 替换 Task 减少堆分配
并发 ConcurrentDictionary 减少锁竞争
IO HttpClient 连接池 减少握手开销

实际优化流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 1. 测量
var stopwatch = Stopwatch.StartNew();
DoWork();
stopwatch.Stop();
Console.WriteLine($"耗时: {stopwatch.ElapsedMilliseconds}ms");

// 2. 使用 BenchmarkDotNet 进行精确基准
var summary = BenchmarkRunner.Run<MyBenchmark>();

// 3. 定位瓶颈
// dotnet-trace collect --profile cpu-sampling

// 4. 优化
// 5. 重新测量,确认改进
// 6. 重复

总结:性能优化是一个持续迭代的过程——测量、定位、优化、再测量。.NET 提供了从 BenchmarkDotNet 到 OpenTelemetry 的完整工具链,让每一个优化决策都有据可依。常见的优化策略包括减少内存分配(池化、Span、值类型)、利用 SIMD 向量化、优化热路径避免闭包和重复属性访问。记住:始终基于数据做优化,不要让猜想代替测量,也不要为了牛毛般的性能提升而牺牲代码的清晰度。

ByteFisher
分享编程技术 · 记录钓鱼乐趣
扫码关注
▸ 扫码关注 ◂
C#高级教程 系列
第 20/20 篇
分享: