You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

在C#中利用ArrayPool复用字符串转字节数组的内存是否可行?

复用字节数组缓解Newtonsoft.Json序列化的GC压力

当然可以!你的场景是高频(60fps)发送大型JSON字符串,每次序列化都创建新的字节数组,确实会让GC疲于奔命。复用ArrayPool<byte>的字节数组是缓解这个问题的核心思路,我针对你现有的两种序列化写法分别给出优化方案:

优化第一种「先序列化字符串再转字节」的写法

你原来的方法先把对象序列化成字符串,再转成字节数组,中间多了一次字符串的内存分配。不过我们可以用数组池来复用转字节时的缓冲区,减少不必要的数组创建:

public static byte[] SerializeJson(DrawDescriptionLayer layer)
{
    var jsonStr = JsonConvert.SerializeObject(layer, js);
    // 估算需要的字节数(UTF-8每个字符最多占3字节,留冗余避免频繁扩容)
    int estimatedByteCount = Encoding.UTF8.GetByteCount(jsonStr);
    // 从共享数组池租用足够大的缓冲区
    byte[] buffer = ArrayPool<byte>.Shared.Rent(estimatedByteCount);
    
    try
    {
        // 直接把字符串写入租用的缓冲区
        int actualByteCount = Encoding.UTF8.GetBytes(jsonStr, 0, jsonStr.Length, buffer, 0);
        
        // 如果必须返回刚好大小的字节数组(比如调用方不支持指定长度)
        // 就复制有效字节到新数组,同时归还原缓冲区
        byte[] result = new byte[actualByteCount];
        Buffer.BlockCopy(buffer, 0, result, 0, actualByteCount);
        return result;
    }
    finally
    {
        // 无论成功失败,都要把缓冲区归还数组池,避免内存泄漏
        ArrayPool<byte>.Shared.Return(buffer);
    }
}

不过要注意:这个方法还是会创建中间字符串jsonStr,高频调用下依然会产生GC压力。如果想彻底减少分配,第二种流序列化的写法优化空间更大。

优化第二种「流直接序列化」的写法

你原来的流写法最后调用ms.ToArray()还是会创建新数组,我们可以让MemoryStream直接使用数组池的缓冲区,序列化完成后直接返回缓冲区和实际写入长度,完全避免额外的数组分配:

// 返回元组:租用的缓冲区 + 实际写入的字节长度
public static (byte[] Buffer, int ByteLength) SerializeJson2(DrawDescriptionLayer layer)
{
    // 初始缓冲区大小可以根据你的JSON平均大小调整,比如4KB
    int initialBufferSize = 4096;
    byte[] buffer = ArrayPool<byte>.Shared.Rent(initialBufferSize);
    
    try
    {
        // 让MemoryStream使用租用的缓冲区,设置publiclyVisible=true允许访问底层数组
        using (var ms = new MemoryStream(buffer, 0, buffer.Length, true, true))
        // 设置leaveOpen=true,避免StreamWriter销毁时关闭MemoryStream
        using (var writer = new StreamWriter(ms, Encoding.UTF8, initialBufferSize, leaveOpen: true))
        using (var jsonWriter = new JsonTextWriter(writer))
        {
            JsonSerializer ser = JsonSerializer.Create(js);
            ser.Serialize(jsonWriter, layer);
            
            // 确保所有数据都写入到MemoryStream
            jsonWriter.Flush();
            writer.Flush();
            
            // 返回缓冲区和实际写入的字节数
            return (buffer, (int)ms.Position);
        }
    }
    catch
    {
        // 序列化出错时,务必归还缓冲区
        ArrayPool<byte>.Shared.Return(buffer);
        throw;
    }
}

使用这个方法时,发送网络数据的时候直接用返回的缓冲区和长度,用完后归还数组池:

var (buffer, length) = SerializeJson2(yourLayer);
try
{
    // 只发送缓冲区的前length个字节
    await yourNetworkStream.WriteAsync(buffer, 0, length);
}
finally
{
    // 用完归还,让数组池复用这个缓冲区
    ArrayPool<byte>.Shared.Return(buffer);
}

额外注意事项

  • 如果你的调用方必须接收完整的byte[],可以在序列化后把有效字节复制到一个更小的租用数组里,但这样会多一次内存复制,尽量优先用「缓冲区+长度」的方式。
  • 关于第三方类型无序列化属性的问题:Newtonsoft.Json可以通过自定义ContractResolver来配置序列化规则,不需要修改第三方代码,比如忽略不需要的属性、指定序列化字段等。如果追求更高性能,也可以考虑轻量二进制序列化库,但Newtonsoft.Json的上述优化已经能大幅缓解GC压力。

内容的提问来源于stack exchange,提问作者thalm

火山引擎 最新活动