如何解读BenchmarkDotNet和dotMemory的结果?

12

所以,我在我的Main()方法中有以下代码:

for (int x = 0; x < 100; x++) // to mimic BenchmarkDotnet runs
   for (int y = 0; y < 10000; y++)
     LogicUnderTest();

接下来,在测试中我有以下的类

[MemoryDiagnoser, ShortRunJob]
public class TestBenchmark
{
    [Benchmark]
    public void Test_1()
    {
        for (int i = 0; i < 10000; i++)
            LogicUnderTest();
    }
}

dotMemory 下运行了约 6 分钟的 Main() 后,我收到了以下结果。

enter image description here

该应用程序从10Mb开始,增加到14Mb
但是,当我运行BenchmarkDotnet测试时,出现以下情况enter image description here
我发现已经分配了2.6GB。这是怎么回事?似乎一点都不好。此外,我看不到Gen1Gen2列。这是否意味着代码没有在它们中分配任何东西,因此没有什么可以显示?
如何解释这些结果?在DotMemory中,它似乎完全正常,但在BenchmarkDotNet中却不正常。我对BenchmarkDotnet还比较新,对于有关结果的任何信息将非常有帮助。
附: LogicUnderTest() 大量使用字符串。
附注:大概来说,LogicUnderTest 的实现如下:
void LogicUnderTest()
{
    var dict = new Dictionary<int, string>();
    for (int j = 0; j < 1250; j++)
        dict.Add(j, $"index_{j}");
    string.Join(",", dict.Values);
}

@mjwills 我已经在测试下添加了该方法的初步实现。 - Semuserable
3个回答

24

我是MemoryDiagnoser的作者,我也在我的博客上回答了你的问题。我会在这里复制粘贴:

如何阅读结果

|     Method |  Gen 0 | Allocated |
|----------- |------- |---------- |
|          A |      - |       0 B |
|          B |      1 |     496 B |
  • Allocated 包含已分配 托管 内存的大小。不包括 stackalloc/native 堆内存分配。这是每个单独调用的 总量
  • Gen X列包含每 1 000 操作中 X 代的垃圾回收次数。如果值等于1,则表示在一千个基准调用中,GC会对 X 代进行一次内存回收。 BenchmarkDotNet 在运行基准测试时使用一些启发式方法,因此不同运行的调用次数可能不同。缩放使结果可比较。
  • -在 Gen 列中表示未执行垃圾回收。
  • 如果没有 Gen X 列,则表示没有针对 X 代进行垃圾回收。如果您的基准测试没有引起 GC,则不会出现 Gen 列。

阅读结果时请注意:

  • 1 kB = 1,024 字节
  • 每个引用类型实例都有两个额外字段:对象头和方法表指针。这就是为什么结果始终包括每个对象分配的 2x 指针大小的原因。有关额外开销的更详细信息,请阅读 Konrad Kokosa 的这篇博客文章 How does Object.GetType() really work?.
  • CLR 进行一些对齐操作。如果您尝试分配 new byte[7] 数组,则会分配 byte[8] 数组。

2

好的,让我们来看一下单个循环迭代:

  • 你将至少要分配1250个int - 所以我们称之为5000字节或5K。
  • 您将创建一个包含相同int和1250个字符串的字典,平均长度为8个字符,因此我们将其称为20000字节或20K。 加上Dictionary本身的开销。
  • 然后string.Join将使用StringBuilder - 因此那里至少会多出20K(由于数组是动态大小的,可能更多)。 然后在StrinBuilder上调用ToString(因此另外20K)。

5K + 20K + 20K + 20K = 65K。

2.86GB / 10,000 = 0.286MB = 约286k。

所以,所有这些听起来都是正确的。65K是RAM使用量的绝对最小值。考虑到生成字典值时的字符串连接开销,使用Dictionary的开销(额外的数组,int的额外副本等)以及StringBuilder的开销(由于字符串长度而可能多次分配大数组),你很容易从65 -> 286。


那么,这是否意味着BenchmarkDotNet显示的最终“Allocated”值是所有可能分配的累积? - Semuserable
基本上是的,@Semuserable。 - mjwills
dotMemory显示这段代码没有任何内存瓶颈,因为GC正在工作,那么它是否意味着在BenchmarkDotNet运行期间GC被关闭了呢?@mjwills - Semuserable

2
BenchmarkDotNet所显示的内容在dotMemory中称为"内存流量"。启用 "立即开始收集分配数据",在dotMemory下运行您的应用程序。在分析会话结束时获取内存快照,然后打开"内存流量"视图。您将看到在分析会话期间分配和回收的所有对象。
至于您关于内存瓶颈的问题,由于所有分配的对象都被收集,内存消耗不会增长,您在dotMemory中也不会看到任何问题。
但每6秒钟3GB的流量相当大,可能会影响性能,请使用dotTrace(时间线模式)查看这6秒钟中的哪一部分用于GC。

“Memory traffic” 显示几乎所有分配的对象都被收集了。 分配的对象 - 897147,收集的对象 - 893655。关于 dotTrace,我发现了一个小瓶颈(感谢建议!),但是内存量加减仍然相同。 - Semuserable
你发现了BenchmarkDotNet报告的2.8GB吗? - Ed Pavlov
据我理解,它是所有运行的累积值。那么,“你找到了”是什么意思?是的,这是代码生成的原始字符串。我认为在实际运行中这不是问题,因为“GC”会收集所有内容。我有什么遗漏吗? - Semuserable
1
我的意思是你在dotMemory/dotTrace中找到了它吗。 你说得对,由于GC收集了所有分配的对象(当然不包括可能存在的性能问题),所以没有内存问题。 很高兴你的问题得到了答复 :) - Ed Pavlov

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接