Visual Studio的.NET内存分配分析器解析

时间:2023-12-09 15:01:25

Visual Studio 2012拥有丰富的有价值的功能,以至于我听到开发者反馈的需要的新功能新版本已经有了。另外,我听到开发人员询问具体的功能的某个特性,实际上他真正需要的是另外一个功能点。

上面说的两种情况下适用于Visual Studio的.NET内存分配分析器 。 许多开发人员可能会从中受益却不知道它的存在,而另外一些开发者有却对它有不正确的理解。 这样很不好,因为该功能可以提供很多有价值的特定场景; 许多开发在理解的情况下才能发挥其预期的作用。也就是要做到以下两点:第一,知道它的存在,第二,知道如何使用。

为什么内存分析?

当谈到.NET和内存分析,有两个主要的原因之一,可能需要使用一个诊断工具:

1. 发现内存泄漏。

2. 发现不必要的分配。

内存优化的实例

为了更好地理解内存分析器的作用,以及它如何帮助到我们。让我们通过一个示例来理解。

  public static async Task<T> WithCancellation1<T>( this Task<T> task, CancellationToken cancellationToken)
 {
     var tcs = new TaskCompletionSource< bool >();
     using (cancellationToken.Register(() => tcs.TrySetResult( true )))
     if (task != await Task.WhenAny(task, tcs.Task))
         throw new OperationCanceledException(cancellationToken);
     return await task;
 }
上面这段代码的作用是可以异步的取消正在运行的任务
 
  T result = await someTask; 
  T result = await someTask.WithCancellation1(token); 

如果取消要求在任务完成之前,相关的CancellationToken,一个OperationCanceledException将被抛出。

要了解参与这一方法的分配,我们将使用一个小程序去测试。
 
using System;
 using System.Threading;
 using System.Threading.Tasks;
 
 class Harness
 {
     static void Main() 
      { 
          Console.ReadLine(); // wait until profiler attaches
          TestAsync().Wait(); 
      }
     static async Task TestAsync()
     {
         var token = CancellationToken.None;
         for ( int i=0; i<100000; i++)
             await Task.FromResult(42).WithCancellation1(token);
     }
 }
 
 
 static class Extensions
 {
     public static async Task<T> WithCancellation1<T>(
     this Task<T> task, CancellationToken cancellationToken)
     {
         var tcs = new TaskCompletionSource< bool >();
         using (cancellationToken.Register(() => tcs.TrySetResult( true )))
             if (task != await Task.WhenAny(task, tcs.Task))
                 throw new OperationCanceledException(cancellationToken);
         return await task;
     }
 } 

运行.NET内存分配分析器

要启动内存分配分析器,在Visual Studio中去分析菜单并选择“启动性能向导...”。 这将打开如下所示的对话框:

Visual Studio的.NET内存分配分析器解析

选择“.NET内存分配(取样)”,单击下一步两次,最后是完成。 在这一点上,应用程序将被启动,并剖析将开始监视它的分配(线束上面的代码也需要你按下“Enter”键) 。 当应用程序完成后,或当你手动选择停止剖析,剖析会加载符号,并开始分析跟踪。 这是一个很好的时间去让自己的一杯咖啡,或午​​餐,因为这取决于有多少分配发生后,该工具可以需要一段时间才能做到这一点的分析。

当分析完成后,我们看下报告:

Visual Studio的.NET内存分配分析器解析

从这里,我们可以进一步研究,通过查看分配汇总(从“当前视图”下拉列表中选择“分配”):

Visual Studio的.NET内存分配分析器解析

在这里,我们能看到一排的被分配的每种类型,同列显示有多少分配被跟踪的信息,有多少空间与分配,以及如何分配映射回该类型的百分比有关。 我们还可以扩展一个条目看,看到这些分配方法的调用堆栈:

Visual Studio的.NET内存分配分析器解析

通过选择“功能”的观点,我们可以得到一个不同的支点这一数据,高亮它的功能分配的大多数对象和字节:

Visual Studio的.NET内存分配分析器解析

剖析分析报告并做相应优化

我们可以分析我们的例子中的结果。 首先,我们可以看到,有相当数量的分配在哪里,这可能是令人惊讶的。毕竟,在我们的例子中我们使用WithCancellation1与已经完成了任务,这意味着应该有很少的工作要做(与已完成的任务,没有什么取消).然而从上面的跟踪我们可以看到,我们的例子中的每一次迭代中产生:

  • 三种分配Task`1的(我们跑了线束10万次,可以看到有〜300K分配)
  • 两个分配task[]

有关任务的辅助业务,它实际上是相当普遍的处理已经完成的任务,因为很多时候,操作执行是异步的,实际上完全同步(例如,在网络流中的一个读操作可以缓冲到内存足够的额外数据来完成后续的读操作)。 因此,优化已经完成的情况下,可以为表现的确有益。 让我们来试试。 这里有一个第二次尝试WithCancellation,一个优化的几个“已完成”的情况:

   public static Task<T> WithCancellation2<T>( this Task<T> task, 
   CancellationToken cancellationToken)
     {
         if (task.IsCompleted || !cancellationToken.CanBeCanceled)
             return task;
         else if (cancellationToken.IsCancellationRequested)
             return new Task<T>(() => default (T), cancellationToken);
         else
             return task.WithCancellation1(cancellationToken);
     } 

此实现检查:

  • 首先,无论是任务已经完成或是否提供的CancellationToken无法取消; 在两者的那些情况下,有不需要额外的工作,因为取消不能适用,因此我们可以只返回原始任务立即而不是花费任何时间或存储器中创建一个新的。
  • 那么是否取消已要求; 如果有,我们可以分配一个业已取消的任务将返回,而不是花费八个分配我们先前支付给调用我们原来的实现。
  • 最后,如​​果没有这些快速通道申请,我们通过降至调用原始的实现。

再分析我们的微基准,而使用的,而不是WithCancellation1 WithCancellation2提供了大大改善的前景(你很可能会发现,分析的速度远远超过它之前,已经是一个迹象,表明我们已经显著减少内存分配完成)。 现在,我们刚刚有主要业务划分,我们预计,从Task.FromResult一个从线束我们TestAsync方法叫做:

Visual Studio的.NET内存分配分析器解析

所以,我们现在已经成功了优化,其中任务已经完成,取消在那里不能要求,或者取消已申请的情况。 怎么样的情况下,我们确实需要调用更复杂的逻辑? 是否有可有什么改进?

让我们改变我们的基准来使用,这不是已经由我们引用WithCancellation2,并且还使用可以有取消请求令牌的时间内完成的任务。 这将确保我们使它的“慢”的路径:

       using (cancellationToken.Register(() => tcs.TrySetResult( true ))) 

剖析再次提供了更深入的了解:

Visual Studio的.NET内存分配分析器解析

在这种缓慢的道路,现在有14%的迭代拨款总额,包括2从我们TestAsync线束(该TaskCompletionSource <int>的我们明确地创建和任务<int>的它创建)。 在这一点上,我们可以使用所有的分析结果提供的信息,了解那里的其余12分配的来源,并随后解决这些问题的是相关的和可能的。 例如,让我们看两个分配具体为:在<> c__DisplayClass2`1实例,这两个动作实例之一。 这两种分配将可能是合乎逻辑的任何人都熟悉的C#编译器如何处理倒闭 。 为什么我们有一个封闭? 由于该行的:

使用(cancellationToken.Register(()=> tcs.TrySetResult( 真 )))

电话注册,是关在'TCS'变量。 但是,这不是严格必需的:该注册方法具有另一个超载,而不是采取一个动作这需要一个Action <object>和该对象的状态被传递给它。 如果我们不是重写此行使用基于状态的过载,以及一个手动缓存的委托,我们能够避免倒闭和这两个分配:

  private static readonly Action< object > s_cancellationRegistration =
     s => ((TaskCompletionSource< bool >)s).TrySetResult( true );
 using (cancellationToken.Register(s_cancellationRegistration, tcs))

重新运行探查证实这两个分配不再发生:

从今天开始分析!

分析,发现和消除热点,然后将再次围绕这个周期是改善代码的性能,常见的方法是否使用CPU分析器和内存分析器。 所以,如果你发现内存分配有可能程序的瓶颈,不妨尝试.NET的内存分配分析器。