【问题标题】:Memory Fragmentation with byte[] in C#在 C# 中使用 byte[] 进行内存碎片
【发布时间】:2021-03-22 11:42:00
【问题描述】:

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

我们使用的代码如下

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,这会对垃圾收集器造成影响

【问题讨论】:

  • 可能在写入数据之前运行 GC 以减少碎片。
  • @jdweng,我们循环调用,每次调用 GC 可能会严重影响 CPU 性能,因为在此操作期间将占用所有其他资源
  • 在循环之前调用 GC。只要为循环中最大的对象清除了足够的连续内存,using 语句就应该处理对象,因此您不应该碎片化。
  • 恐怕这可能无济于事,因为由于内部逻辑而发生碎片并且我们已经对每个 IDisposable 对象进行了处置。所以在外面调用 GC 可能会产生零到可以忽略不计的影响。并不总是建议显式调用 GC。

标签: c# arrays memory-management memory-fragmentation


【解决方案1】:

这里有两个不同的东西:

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

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

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

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, 0, buffer.Length)) > 0)
{...}

改为尝试:

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

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

【讨论】:

  • @HattedRooster 简单的答案是:考虑每一行代码:它做什么,为什么这样做,这是最好的方法,有什么含义;而不是大多数人似乎做的就是从Stack Overflow复制意大利面
猜你喜欢
  • 1970-01-01
  • 2013-08-14
  • 1970-01-01
  • 2019-06-10
  • 2010-10-26
  • 2013-05-11
  • 2012-11-26
  • 1970-01-01
  • 2018-09-25
相关资源
最近更新 更多