很多服务在压测里吞吐不错,一上生产就抖。根因常常不是业务逻辑,而是内存分配模式不健康。
典型表现:
- GC 频率偏高
- P99 周期性尖峰
- 单机吞吐不稳定
这篇文章重点讲"如何减少无意义分配",并把优化控制在团队可维护范围内。
问题背景
真实现场:一次常规版本上线后,接口平均耗时变化不大,但 P99 从 120ms 拉到 380ms,最后定位到的是字符串拼接和临时数组在高峰时段触发了更频繁的 GC。
一个日志清洗或协议解析接口,经常会写出这样的代码:
csharp
var parts = line.Split('|');
var user = parts[0].Trim();
var action = parts[1].Trim();
var ts = DateTime.Parse(parts[2]);
逻辑简单,但每次请求都在制造短命对象。高并发下,这些小分配会形成大压力。
原理解析
代际 GC
短命对象主要在 Gen0/Gen1 回收。分配速率过高时,GC 会频繁触发,吞吐被打断。
LOH(大对象堆)
大于约 85KB 的对象进入 LOH。大对象频繁创建和释放,会导致更重的回收成本和内存碎片问题。
Span<T> 与 ArrayPool<T>
Span<T>在不额外分配的前提下切片处理内存ArrayPool<T>复用数组,避免重复 new 大缓冲区
示例代码
先定义记录:
csharp
public sealed record AuditLog(string User, string Action, DateTime Timestamp);
使用 ReadOnlySpan<char> 解析:
csharp
public static bool TryParseAuditLog(string line, out AuditLog? log)
{
log = null;
ReadOnlySpan<char> span = line.AsSpan();
var p1 = span.IndexOf('|');
if (p1 < 0) return false;
var p2 = span[(p1 + 1)..].IndexOf('|');
if (p2 < 0) return false;
p2 += p1 + 1;
var user = span[..p1].Trim();
var action = span[(p1 + 1)..p2].Trim();
var tsText = span[(p2 + 1)..].Trim();
if (!DateTime.TryParse(tsText, out var ts)) return false;
log = new AuditLog(user.ToString(), action.ToString(), ts);
return true;
}
对于大文本拼接,使用池化数组:
csharp
using System.Buffers;
using System.Text;
public static string BuildCsvLine(IReadOnlyList<string> columns)
{
var pool = ArrayPool<char>.Shared;
var buffer = pool.Rent(4096);
try
{
var pos = 0;
for (var i = 0; i < columns.Count; i++)
{
var col = columns[i].AsSpan();
col.CopyTo(buffer.AsSpan(pos));
pos += col.Length;
if (i < columns.Count - 1)
{
buffer[pos++] = ',';
}
}
return new string(buffer, 0, pos);
}
finally
{
pool.Return(buffer, clearArray: true);
}
}
工程实践建议
先定位热点,再做低层优化
优先用以下工具定位:
dotnet-countersdotnet-tracePerfView或SpeedScope
不要把 Span<T> 当"默认写法"。
池化有收益,也有代价
ArrayPool 会提升复杂度。只有在高频路径或大数组场景才值得引入。
限制 LOH 对象生成
- 避免一次性拼接超大字符串
- 大缓冲优先池化
- 流式处理替代整块加载
团队约束建议
建立两层规范:
- 普通代码优先可读性
- 热路径允许低层优化,但必须配压测数据
评论区讨论
- 你们线上更常见的是 CPU 打满,还是 GC 抖动导致的延迟尖峰?
- 在你的团队里,
Span<T>属于默认工具,还是只在热点路径使用? - 如果一个优化能省 8% 分配但明显降低可读性,你会怎么取舍?
总结
内存优化不是炫技,目标是把分配曲线压平,让系统在高峰期也能稳定响应。
Span<T> 和 ArrayPool<T> 非常有价值,但前提是你清楚热点在哪、收益有多大、复杂度是否可接受。工程上,量化收益永远比"看起来高级"更重要。