【问题标题】:How to interpret the results from BenchmarkDotNet and dotMemory?如何解释 BenchmarkDotNet 和 dotMemory 的结果?
【发布时间】:2019-02-12 20:59:46
【问题描述】:

所以,我的 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 下运行Main() 大约 6 分钟后,我收到以下结果

应用程序从10Mb 开始,一直到14Mb

但是当我运行 BenchmarkDotnet 测试时,我得到了这个

我看到我已经分配了2.6GB。什么?似乎一点都不好。另外,我看不到 Gen1Gen2 列。这是否意味着代码没有在其中分配任何内容,因此没有任何内容可显示?

如何解释结果?在DotMemory 中似乎完全正常,但在BenchmarkDotNet 中则不行。我是BenchmarkDotnet 的新手,对于有关结果的任何信息都会有所帮助。

附言。 LogicUnderTest() 广泛用于字符串。

PSS。大概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 我已经添加了被测方法的粗略实现。

标签: c# .net benchmarking dotmemory benchmarkdotnet


【解决方案1】:

我是MemoryDiagnoser 的作者,我还在blog 上提供了您问题的答案。我将在这里复制过去:

如何读取结果

|     Method |  Gen 0 | Allocated |
|----------- |------- |---------- |
|          A |      - |       0 B |
|          B |      1 |     496 B |
  • Allocated 包含分配的托管内存的大小。 不包括 Stackalloc/本机堆分配。这是每次调用,包括
  • Gen X 列包含每个 1 000 操作的 Gen X 集合数。如果该值等于 1,则表示 GC 在生成 X 中每千次基准调用收集一次内存。 BenchmarkDotNet 在运行基准测试时使用了一些启发式方法,因此不同运行的调用次数可能不同。缩放使结果具有可比性。
  • Gen 列中的- 表示未执行垃圾回收。
  • 如果Gen X 列不存在,则意味着没有为生成X 执行垃圾收集。如果您的任何基准测试均未引发 GC,则 Gen 列不存在。

阅读结果时请记住:

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

【讨论】:

    【解决方案2】:

    BenchmarkDotNet 向您展示的内容在 dotMemory 中称为“内存流量”。在启用“Start collecting allocation data immediately”的情况下在 dotMemory 下运行您的应用程序。在分析会话结束时获取内存快照,然后打开“Memory Traffic”视图。您将看到在分析会话期间分配和收集的所有对象。

    关于内存瓶颈的问题怎么样,因为所有分配的对象都被收集了,内存消耗不会增加,并且您在 dotMemory 中看不到任何问题。

    但是每 6 秒 3GB 的流量相当大,可能会对性能产生影响,请使用 dotTrace(在时间线模式下)查看这 6 秒的哪一部分用于 GC。

    【讨论】:

    • Memory traffic 表明几乎所有分配的对象都被收集了。 Allocated objects - 897147,Collected objects - 893655。关于dotTrace,我发现了一个小瓶颈(感谢您的建议!),但内存量仍然加\减相同。
    • @Semuserable 你找到 BenchmarkDotNet 报告的 2.8GB 了吗?
    • 据我了解,这是所有运行的累积值。那么,“你找到了吗”是什么意思?是的,它是代码生成的原始字符串。正如我所见,在实际运行中这不是问题,因为GC 收集了所有内容。我错过了什么吗?
    • 我的意思是你在 dotMemory/dotTrace 中找到了它。你是对的,没有内存问题,因为 GC 收集了所有分配的对象,当然不包括可能的性能问题。很高兴你的问题得到了答案:)
    【解决方案3】:

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

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

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

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

    所以,所有这些听起来都是对的。 65K 是 RAM 使用量的绝对最小值。在生成字典值时考虑字符串连接开销、使用Dictionary 的开销(额外的数组、ints 的额外副本等)和StringBuilder 的开销(这可能会为大型数组分配一个数字次由于字符串的长度),你可以很容易地从 65 -> 286 得到。

    【讨论】:

    • 那么,这是否意味着BenchmarkDotNet 显示的最终Allocated 值是所有可能分配的累积?
    • 基本上是的@Semuserable。
    • dotMemory 向我展示了这段代码没有任何内存瓶颈,因为GC 完成了它的工作?这是否也意味着GCBenchmarkDotNet 运行期间关闭? @mjwills
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-02-06
    • 2014-07-10
    • 2017-05-02
    • 2017-04-25
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多