在C#开发中,性能优化并不是上线前的临时补救措施,而是贯穿设计、编码与测试的系统工程。很多项目的接口延迟突增、内存占用居高不下,根源往往藏在看似平常的语法糖背后。本文从分配控制、数据结构选型和并发模型三个维度,拆解可落地的调优方法。

减少堆分配与避免装箱拆箱
CLR的垃圾回收器虽然自动管理内存,但每一次堆分配都会增加Gen0压力,频繁分配会触发更频繁的GC停顿。最常见的隐性分配来自装箱:当值类型被转换为object或传入接受object参数的方法时,会在堆上创建包装对象。例如ArrayList或早期Hashtable存储int就会产生大量装箱。现代代码应优先使用泛型集合List<int>,从类型系统层面消除装箱。
另一个容易被忽视的分配点是字符串拼接与闭包。在循环中使用+拼接字符串会生成多个临时string对象,应改用StringBuilder。当lambda捕获局部变量时,编译器可能生成内部类导致分配,在热路径上可考虑提取为静态方法。下面的代码展示了装箱与避免装箱的对照:
using System;
using System.Collections.Generic;
class Program
{
// 产生装箱的写法
static void BoxExample()
{
var list = new List<object>();
for (int i = 0; i < 1000; i++)
{
list.Add(i); // int 装箱为 object
}
}
// 避免装箱的写法
static void NoBoxExample()
{
var list = new List<int>();
for (int i = 0; i < 1000; i++)
{
list.Add(i); // 无装箱
}
}
static void Main()
{
BoxExample();
NoBoxExample();
}
}
除了显式装箱,某些API也会暗中分配。例如Enum.ToString()在高频调用时成本不低,可缓存结果或使用nameof。对于必须返回字符串的格式化场景,string.Format在.NET Core之后已高度优化,但仍建议用插值字符串并观察是否产生分配。借助BenchmarkDotNet可以精确测量每次调用的字节分配量,从而定位真正的元凶。
利用结构体、Span与栈上分配优化数据访问
值类型结构体在栈或内联存储时无需GC介入,适合小型、不可变的数据载体。但若结构体过大或作为方法参数频繁拷贝,反而会降低性能。经验法则是将小于16字节且语义清晰的类型定义为struct,如坐标、颜色值。对于需要避免拷贝又希望连续内存的场景,可使用ref struct与Span<T>,它们提供类似数组的视图却不持有底层缓冲,极大减少中间数组分配。
Span<T>与Memory<T>是.NET Core引入的零分配切片工具。解析网络协议或处理大字符串时,传统Substring会复制字符数组,而string.AsSpan().Slice()仅创建轻量视图。以下示例展示用Span<char>切分日志行而不产生新字符串:
using System;
class LogParser
{
static void Parse(ReadOnlySpan<char> line)
{
int idx = line.IndexOf('|');
if (idx < 0) return;
var time = line.Slice(0, idx);
var body = line.Slice(idx + 1);
Console.WriteLine(time.Length);
Console.WriteLine(body.Length);
}
static void Main()
{
ReadOnlySpan<char> sample = "2024-01-01 12:00|error occurred".AsSpan();
Parse(sample);
}
}
栈上分配通过stackalloc在方法内部申请栈内存,适合短生命周期的小缓冲。但栈空间有限,不可在异步方法或返回后引用。结合Span<T>使用能兼顾安全与效率。需要提醒的是,结构体若包含引用类型字段,其仍然会带来追踪开销;定义时尽量保持纯值字段。对于集合批量处理,使用ArrayPool<T>.Shared租借数组可复用缓冲,降低Loh碎片。
并行化与异步改造提升吞吐
多核时代,单线程循环常常成为瓶颈。对于相互独立的CPU密集任务,可用Parallel.For或Task.WhenAll将工作分摊到线程池。但要注意线程竞争:共享字典应改用ConcurrentDictionary<TKey,TValue>,或采用分片锁减少冲突。并行度并非越高越好,过度并行会引发上下文切换与缓存失效,建议通过基准测试确定最佳MaxDegreeOfParallelism。
IO密集场景应使用异步方法而非新建线程。将同步Read改为ReadAsync并配合await,能释放线程处理其他请求,显著提升Web服务并发。但需避免在热路径上滥用Task.Run包装本就异步的调用,那只会徒增调度成本。下面的例子对比了同步处理与并行处理大集合的差异:
using System;
using System.Linq;
using System.Threading.Tasks;
class Compute
{
static int Heavy(int x) => x * x;
static void SyncProcess()
{
var sum = Enumerable.Range(0, 100000)
.Select(Heavy)
.Sum();
Console.WriteLine(sum);
}
static void ParallelProcess()
{
int sum = 0;
Parallel.For(0, 100000, i =>
{
var v = Heavy(i);
System.Threading.Interlocked.Add(ref sum, v);
});
Console.WriteLine(sum);
}
static async Task Main()
{
SyncProcess();
ParallelProcess();
await Task.Delay(0);
}
}
异步流IAsyncEnumerable<T>适合逐条产出大量数据的管道,能在消费端按需拉取,控制内存峰值。对于后台长任务,应区分CPU密集与IO密集,分别选用TaskScheduler或IO完成端口。最后,任何并行与异步改造都必须配合监控:用dotnet-counters观察线程池吞吐与GC频率,确认优化真实生效而非仅停留在代码表象。