8

所以,我的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();
    }
}

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

在此处输入图像描述

该应用程序从 开始10Mb并上升到14Mb

但是当我运行BenchmarkDotnet测试时,我得到了这个 在此处输入图像描述

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

我如何解释结果?在 .中似乎完全可以DotMemory,但在BenchmarkDotNet. 我很新,BenchmarkDotnet对有关结果的任何信息都有帮助。

PS。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);
}
4

3 回答 3

13

我是的作者,MemoryDiagnoser我也在我的博客上提供了你问题的答案。我将在这里复制过去:

如何读取结果

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

阅读结果时请记住:

  • 1 kB = 1 024 字节
  • 每个引用类型实例都有两个额外的字段:对象头和方法表指针。这就是为什么结果总是包含每个对象分配的 2 倍指针大小。有关额外开销的更多详细信息,请阅读这篇很棒的博客文章Object.GetType() 如何真正起作用?通过康拉德科科萨。
  • CLR 会进行一些对齐。如果您尝试分配new byte[7]数组,它将分配byte[8]数组。
于 2018-09-10T10:15:37.720 回答
2

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

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

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

于 2018-09-10T09:18:38.433 回答
1

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

  • 您将至少分配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(额外数组、s 的额外副本int等)的开销和StringBuilder(由于字符串的长度可能会多次分配大型数组)的开销) 你可以很容易地从 65 -> 286 得到。

于 2018-09-07T22:44:04.223 回答