微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

在 C# 中使用 byte[] 进行内存碎片

如何解决在 C# 中使用 byte[] 进行内存碎片

我正在开发的 C#/.NET 应用程序使用了巨大的字节数组并且存在内存碎片问题。使用 CLRMemory 检查内存使用情况

please ref the image for LOH and Free Space

我们使用的代码如下

PdfLoadedDocument loadedDocument = new PdfLoadedDocument("myLoadedDocument.pdf");

// Operations on pdf document

using (var stream = new MemoryStream())
{
    loadedDocument.Save(stream);
    loadedDocument.Close(true);
    return stream.ToArray(); //byte[]
}

我们在应用程序的多个地方使用了类似的代码,我们在循环中调用它来生成从几百个到 10000 个不等的批量审计

  1. 现在有更好的方法来避免碎片化

作为审计的一部分,我们还使用以下代码从 Amazon S3 下载大文件

using (var client = new AmazonS3Client(_accessKey,_secretKey,_region))
{
   var getobjectRequest = new GetobjectRequest();
   getobjectRequest.BucketName = "bucketName";
   getobjectRequest.Key = "keyName";

   using (var downloadStream = new MemoryStream())
   {
       using (var response = await client.GetobjectAsync(getobjectRequest))
       {
           using (var responseStream = response.ResponseStream)
           {
               await responseStream.copyToAsync(downloadStream);
           }
           return downloadStream.ToArray(); //byte[]
       }
   }
}
  1. 是否有更好的替代方法来下载大文件,而无需将它们转移到 LOH,这会对 Garbage Collector 造成影响

解决方法

这里有两件不同的事情:

  1. MemoryStream 的内部结构
  2. .ToArray() 的用法

对于 MemoryStream 内部发生的事情:它实现为一个简单的 byte[],但是您可以通过使用 RecyclableMemoryStream 来减轻很多开销而是通过 Microsoft.IO.RecyclableMemoryStream nuget 包,它在独立使用之间重用缓冲区。

对于ToArray(),坦率地说:不要那样做。使用原版 MemoryStream 时,更好的方法是 TryGetBuffer(...),它为您提供超大 后备缓冲区以及开始/结束标记:

if (!memStream.TryGetBuffer(out var segment))
    throw new InvalidOperationException("Unable to obtain data segment; oops?");
// see segment.Offset,.Count,and .Array

然后你的工作是不要超出这些界限。如果你想让这更容易:考虑将段视为跨度(或内存):

ReadOnlySpan<byte> muchSafer = segment;
// now you can't read out of bounds,and you don't need to apply the offset yourself

然而,这种 TryGetBuffer(...) 方法不适用于 RecyclableMemoryStream - 因为它制作了防御性副本以防止独立数据出现问题;在那个场景中,您应该简单地将流视为流,即Stream - 只需写入它,倒带它(Position = 0),并让消费者从中读取,然后在完成后将其丢弃。


附带说明:使用 Stream API 读取(或写入)时:考虑使用数组池作为暂存缓冲区;所以而不是:

var buffer = new byte[1024];
int bytesRead;
while ((bytesRead = stream.Read(buffer,buffer.Length)) > 0)
{...}

改为尝试:

var buffer = ArrayPool<byte>.Shared.Rent(1024);
try
{
    int bytesRead;
    while ((bytesRead = stream.Read(buffer,buffer.Length)) > 0)
    {...}
}
finally
{
    ArrayPool<byte>.Shared.Return(buffer);
}

在更高级的场景中,使用 pipelines API 而不是 stream API 可能更明智;这里的重点是管道允许不连续缓冲区,因此即使在处理复杂场景时,您也永远不需要大得离谱的缓冲区。然而,这是一个小众 API,并且在公共 API 中的支持非常有限。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。