【问题标题】:c# how can I sidestep the memory allocation bottleneck to improve multithreading performancec#如何绕过内存分配瓶颈来提高多线程性能
【发布时间】:2013-08-07 01:18:50
【问题描述】:

我使用 C# 作为研究工具,并且经常需要运行 CPU 密集型任务,例如优化。从理论上讲,我应该能够通过多线程我的代码来获得很大的性能改进,但在实践中,当我使用与工作站上可用的内核数相同的线程数时,我通常会发现 CPU 仍然只运行在 25最大的 %-50%中断代码以查看所有线程在做什么强烈表明内存分配是瓶颈,因为大多数线程将等待new 语句执行。

一种解决方案是尝试重新设计我的所有代码以提高内存效率,但这将是一项艰巨且耗时的任务。但是,由于我的工作站上有大量内存,我想知道是否可以通过设置不同的线程来回避这个问题,以便它们每个都有自己的私有内存池来工作。当然,有些对象仍然需要在所有线程之间公开,否则无法为每个线程指定任务或获取结果。

有谁知道这种方法在 C# 中是否可行,如果可以,我应该怎么做?

【问题讨论】:

  • 内存管理是由.Net框架完成的,你能做的不多。但是,您确定瓶颈不在 IO 或其他子系统中吗?
  • @oscar 是的,我确信这是内存管理,因为我设置了优化问题,所有数据都保存在内存中,所以我的线程都不需要做任何事情IO 随便。这也是合理的,因为只有一个堆可以创建所有对象。
  • 你有很多内存并构建 x64 并且仍然有分配作为瓶颈?您看到/期待什么样的内存使用情况?
  • @Stochastically 比这复杂一点。它更像是“如果编译器可以证明引用没有超出范围,它通常会在堆栈上分配”。这适用于类和结构——它仅取决于对象的实际使用情况。不同之处在于,由于结构变量本身传递数据(而不是引用),它们更有可能满足堆栈分配的标准。 (但请记住,这整个事情是依赖于实现的,因为堆栈和堆本身实现细节。)
  • @Stochasticly 还有unsafe 块,您可以在其中使用stackalloc 并明确控制分配发生的位置(但它仅适用于结构实例,不适用于类实例)。确实,这不是一个非常干净的解决方案,尤其是如果它妨碍了代码的语义,这就是我将其作为评论的原因。不过,作为最后的手段,它可能会对您有所帮助。

标签: c# multithreading performance memory-management heap-memory


【解决方案1】:

如果你有内存分配瓶颈,你应该:

  1. 使用“对象池”(正如@MartinJames 所说)。在应用程序启动时初始化对象池。对象池应该提高堆分配的性能。

  2. 使用结构(或任何值类型)作为局部变量,因为堆栈分配比堆快得多。

  3. 避免隐式内存分配。例如,当您将项目添加到List<>

    如果 Count 已经等于 Capacity,则 List 的容量为 通过自动重新分配内部数组而增加,并且 现有元素在新元素之前被复制到新数组中 已添加(来源 MSDN)。

  4. 避免拳击。很贵:

    对于简单的分配,装箱和拆箱是 计算昂贵的过程。当一个值类型被装箱时,一个新的 对象必须被分配和构造。在较小程度上,演员 拆箱所需的计算成本也很高。 (来源 MSDN)

  5. 避免捕获变量的 lambda 表达式(因为将为捕获的变量创建新对象)

【讨论】:

    【解决方案2】:

    这与我在服务器中所做的类似 - 将对象池用于经常使用的类(尽管不是在 C# 中)。

    我想,在 C# 中,您可以使用 BlockingCollection。用其中的 T 和 Take() 对象预先填充它,使用它们,然后用 Add() 返回。

    这适用于大量且大型的对象(例如,服务器数据缓冲区),或具有复杂而冗长的 ctors/dtors(例如,http 接收器/解析器组件) - 弹出/推送此类对象,(' cos 本质上是 NET 中的指针),off/on 队列比不断创建它们然后让 GC 销毁它们要快得多。

    注意:从此类池队列中弹出的对象可能以前已使用过,可能需要一些显式初始化!

    【讨论】:

    • 感谢@MartinJames,事实上我现在已经在我的一些代码中实现了这种想法,但突然让我震惊的是,如果可以强制 C# 在不同的情况下使用不同的内存堆那样会方便很多。
    • 可以内置一些便利 :) 我所有的池化对象都派生自一个基类,该基类具有一个“Pool myPool”私有成员和一个将对象推回的无参数“release()”方法- 无需传递池实例,也没有机会将对象推回错误的池中。如您所知,私有堆的问题在于线程间通信:如果将从私有池分配的对象排队到另一个线程,则需要池锁才能将其删除,因此破坏了私有堆的优势。
    【解决方案3】:

    这不是一个特别的 C# 或 .NET 问题。为了使 CPU 内核以最佳方式运行,它需要其所有数据都在 CPU 缓存中。如果特定数据不在 CPU 缓存中,则会发生缓存故障并且 CPU 处于空闲状态,直到数据从内存中提取到缓存中。

    如果您的内存数据碎片过多,缓存故障的机会就会增加。

    CLR 进行堆分配的方式更适合 CPU 缓存。您不太可能通过自己处理内存分配来获得相同的性能。

    【讨论】:

    • 我不想自己处理分配。我只是希望能够指示 C# 拥有多个内存堆,以便在创建新对象时不同的线程不需要相互等待。
    猜你喜欢
    • 2010-10-03
    • 2012-05-14
    • 1970-01-01
    • 2014-01-02
    • 1970-01-01
    • 2011-05-18
    • 1970-01-01
    • 1970-01-01
    • 2012-03-05
    相关资源
    最近更新 更多