.NET 内存性能实战:Span<T>、ArrayPool、GC 与 LOH 控制

很多服务在压测里吞吐不错,一上生产就抖。根因常常不是业务逻辑,而是内存分配模式不健康。

典型表现:

  • 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-counters
  • dotnet-trace
  • PerfViewSpeedScope

不要把 Span<T> 当"默认写法"。

池化有收益,也有代价

ArrayPool 会提升复杂度。只有在高频路径或大数组场景才值得引入。

限制 LOH 对象生成

  • 避免一次性拼接超大字符串
  • 大缓冲优先池化
  • 流式处理替代整块加载

团队约束建议

建立两层规范:

  • 普通代码优先可读性
  • 热路径允许低层优化,但必须配压测数据

评论区讨论

  • 你们线上更常见的是 CPU 打满,还是 GC 抖动导致的延迟尖峰?
  • 在你的团队里,Span<T> 属于默认工具,还是只在热点路径使用?
  • 如果一个优化能省 8% 分配但明显降低可读性,你会怎么取舍?

总结

内存优化不是炫技,目标是把分配曲线压平,让系统在高峰期也能稳定响应。

Span<T>ArrayPool<T> 非常有价值,但前提是你清楚热点在哪、收益有多大、复杂度是否可接受。工程上,量化收益永远比"看起来高级"更重要。

相关推荐
少控科技24 分钟前
小数典应用:小诗典
windows·c#
wuyoula1 小时前
尹之盾企业版网络验证
服务器·开发语言·javascript·c++·人工智能·ui·c#
zdr尽职尽责2 小时前
Untiy 处理Aseprite 资产 解决偏移问题
学习·unity·c#·游戏引擎
步步为营DotNet2 小时前
.NET 11 与 C# 14 助力云原生应用安全架构升级
云原生·c#·.net
少控科技3 小时前
小数典应用:农场环境数据采集监控
开发语言·windows·c#
¥-oriented4 小时前
记录使用C#编程中遇到的一个小bug
c#·bug
唐青枫4 小时前
C#.NET MemoryMarshal 深入解析:零拷贝内存重解释、二进制读写与使用边界
c#·.net
成都易yisdong19 小时前
纬地、鸿业、海地、CASS等横断面数据互转工具V3.2——测绘与道路设计人员的效率神器
c#·visual studio code
AIKZX1 天前
西门子博途 TIA Portal v18 中文版图文安装教程(超级详细)附下载链接
开发语言·c#·编辑器·idea
xiaoshuaishuai81 天前
C# 数字资源分发
开发语言·c#