【问题标题】:C# performance varying due to memoryC# 性能因内存而异
【发布时间】:2012-04-17 08:18:39
【问题描述】:

希望这是一篇有效的帖子,它结合了 C# 问题和硬件。

我正在对我们的服务器进行基准测试,因为我们发现了 quant 库(用 C# 编写)的性能问题。我用一些简单的 C# 代码模拟了相同的性能问题——执行非常重的内存使用。

下面的代码是从线程池中生成的一个函数,最多有 32 个线程(因为我们的服务器有 4 个 CPU,每个有 8 个内核)。

这一切都在 .Net 3.5 上

问题是我们得到了截然不同的性能。我运行以下函数 1000 次。代码运行的平均时间可能是 3.5 秒,但最快的只有 1.2 秒,最慢的只有 7 秒——对于完全相同的功能!

我已经根据时间绘制了内存使用情况,似乎与 GC 启动没有任何关联。

我确实注意到的一件事是,在单线程中运行时,时间是相同的,并且没有明显的偏差。我还测试了受 CPU 限制的算法,并且时间也相同。这让我们怀疑内存总线是否无法应对。

我想知道这可能是另一个 .net 或 C# 问题,还是与我们的硬件有关?如果我使用 C++ 或 Java,这会是同样的体验吗?我们使用的是 4x Intel x7550 和 32GB 内存。一般有什么办法可以解决这个问题吗?

Stopwatch watch = new Stopwatch();
watch.Start();
List<byte> list1 = new List<byte>();
List<byte> list2 = new List<byte>();
List<byte> list3 = new List<byte>();


int Size1 = 10000000;
int Size2 = 2 * Size1;
int Size3 = Size1;

for (int i = 0; i < Size1; i++)
{
    list1.Add(57);
}

for (int i = 0; i < Size2; i = i + 2)
{
    list2.Add(56);
}

for (int i = 0; i < Size3; i++)
{
    byte temp = list1.ElementAt(i);
    byte temp2 = list2.ElementAt(i);
    list3.Add(temp);
    list2[i] = temp;
    list1[i] = temp2;
}
watch.Stop();

(代码只是为了强调内存)

我会包含线程池代码,但我们使用了非标准线程池库。

编辑:我已将“size1”减少到 100000,这基本上不会使用太多内存,但我仍然会得到很多抖动。这表明不是传输的内存量,而是内存抢占的频率?

【问题讨论】:

  • 在您的基准测试期间是否有任何其他进程正在运行?甚至操作系统也需要 CPU 时间。如果您在基准测试期间使用所有虚拟内核,那么您实际上(请原谅双关语)保证在您的测试期间不相关的进程将占用 CPU 时间。
  • 除了推测之外,我们没有足够的信息来做任何事情。也就是说,我的钱花在你的“非标准线程池库”上,没有分配足够的线程来并行运行它。如果您运行 50 个副本并且仅分配 20 个线程(例如),则 10 次迭代将不得不(平均)等待 2 次其他迭代完成以释放线程。这可能是您看到的偏差的原因。
  • 只是一个想法:由于您似乎知道列表的大小,因此您应该将其传递给构造函数(或仅使用数组)。然后,如果基础数组,则避免重新分配。
  • 线程之间有同步吗?
  • +1 对 Brian Rasmussen 的建议:您可能在内存分配和移动内容上花费了大量时间。

标签: c# .net performance memory memory-management


【解决方案1】:

没有足够的继续,但这里有一些领域可以开始寻找:

  • 可变性是内部 GC 状态的结果。 GC 动态管理各种池的大小。如果您从不同的池大小开始,您将在运行期间获得不同的 GC 行为。
  • 线程调度中的摩尔纹图案。根据线程顺序的随机变化,您可能会有或多或少有利的争用模式。如果存在任何周期性,则可能会导致类似于相长干涉的放大效应。
  • 虚假共享。如果您有两个线程都命中足够接近的内存地址以位于处理器缓存中,您将看到性能显着下降,因为处理器必须花费大量时间重新同步它们的缓存。根据您组织数据和分配线程来处理数据的方式,您可能会在开始时根据变化获得虚假共享模式。
  • 系统中的另一个进程正在占用处理器时间。您可能希望使用进程用户模式时间而不是挂壁时间的度量。 (Process 类中有一个访问器)。
  • 机器正在运行接近它的完整物理内存限制。以或多或少的随机模式交换到磁盘。

【讨论】:

  • #3 通常称为false sharing
  • @RonWarholic 谢谢。我知道它有一个术语,只是不记得了。
【解决方案2】:

您在这里遇到了非常基本的机器限制。您有很多内核,但仍然只有一个内存总线。因此,如果您的线程进行大量数据混洗,那么它们很可能会受到单条总线带宽的限制。这是阿姆达尔定律在起作用。

有一种可能的优化,它取决于这台机器运行的操作系统的类型。这是服务器类型的硬件,但如果您有非服务器版本的 Windows,那么垃圾收集器将以工作站模式运行。然后,您可以使用应用程序的 .config 文件中的 &lt;gcServer&gt; 元素来询问收集器的服务器版本。它使用多个堆,因此线程在分配内存时不会经常争夺 GC 堆锁。嗯嗯。

【讨论】:

    【解决方案3】:

    List 在内部使用数组进行存储。我相信每次达到列表中可用空间的限制时,它都会尝试将数组的大小加倍。

    当您进入循环时,随着列表的增长,它需要越来越大的连续内存块来分配新数组。用一个线程,这很容易。使用 2 个以上的线程,您正在争夺大块的连续内存。随着数组变得更大并且更难找到连续的内存,它会随机触发 GC。

    【讨论】:

    • 嗨,我已经更改了预定大小 byte[] 的列表,其中大小为 10,000,000,函数完成的时间仍然是完全随机的。最快的是 462 毫秒,平均是 1192 毫秒,最慢的是 2509 毫秒——是平均值的两倍多。
    【解决方案4】:

    确保运行时配置具有 gcserver=true

    【讨论】:

    • 已调查 - 它使过程平均速度更快,但并没有减少时间变化。
    • 我很想看看在你的代码中使用 parallel.for 的结果,看看异步调用的影响
    【解决方案5】:

    在这一点上,猜测任何事情似乎都只是猜测。您真正需要的是更多信息。

    我会连接一个分析器或设置一些 Windows 性能计数器:

    http://support.microsoft.com/kb/300504

    您应该能够添加一些以进程为中心的性能计数器。您可以查看正在启动的线程数、内存利用率等。我会在这里采纳其他一些建议并衡量您正在寻找的场景。如果您将性能计数器数据转储到 csv 文件中,您甚至可以非常快速地绘制结果图表,以获得一些好的数据来实际使用。如果您能发现 1.2s 与 7s 场景中的指标发生了哪些变化,您就可以开始对正在发生的事情进行一些有根据的猜测,并继续磨练。

    【讨论】:

      【解决方案6】:

      对共享资源(如控制台或文件系统)的同步调用会显着降低性能,但从表面上看,这段代码只是在最大化 CPU,时间差异一定是由于其他进程请求 CPU 时间。

      【讨论】:

        猜你喜欢
        • 2012-04-01
        • 1970-01-01
        • 2013-12-04
        • 2022-01-08
        • 1970-01-01
        • 1970-01-01
        • 2020-03-02
        • 2015-07-16
        • 2016-05-29
        相关资源
        最近更新 更多