如何规避内存分配瓶颈以提高多线程性能

6
我使用C#作为一个研究工具,经常需要运行CPU密集型任务,例如优化。理论上,通过多线程编写代码,我应该能够获得大的性能提升,但实际上,当我使用与我的工作站可用核心数量相同的线程数时,通常发现CPU仍然只运行在最高性能的25%-50%。中断代码以查看所有线程正在执行的操作强烈表明内存分配是瓶颈,因为大多数线程将等待new语句执行。
一个解决方案是尝试重新设计所有代码以使其更加内存高效,但这将是一个庞大而耗时的任务。然而,由于我的工作站有充足的内存,我想知道是否可以通过设置不同的线程,使它们各自拥有私有的内存池来避开这个问题。当然,某些对象仍然需要在所有线程之间共享,否则将无法指定每个线程的任务或收集结果。
有人知道在C#中是否可以采用这种方法,如果可以,应该如何做呢?

@AlexeiLevenkov 是的,我使用Windows 7的x64,因为我经常会在运行x86时遇到OutOfMemoryException。像C#这样的语言为程序员完成所有内存分配,鼓励程序员专注于更高级别的目标,而不是低级别的内存管理效率。但是,如果只有一个堆用于分配新对象,那显然可能成为多线程瓶颈,实际上当我中断我的代码时,有很多未决的“new”语句。 - Stochastically
你真的需要将大部分数据放在堆上吗?还是将它们分配到栈上可以接受?如果后者是情况,使用结构体而不是类可能会提高一些速度。 - Theodoros Chatzigiannakis
@TheodorosChatzigiannakis,您是在说“new struct()”被分配到堆栈上,而“new class()”被分配到全局堆上吗?如果是这样的话,我不知道,所以确实可能有帮助。但是,问这个问题的目的是为了尝试找出是否可以强制C#在不同情况下使用不同的全局堆,这对我来说将是一个更清洁的解决方案。 - Stochastically
1
@Stochastically 这比那要复杂一些。更多的是“如果编译器可以证明引用不会逃出作用域,它通常会在堆栈上分配”。这适用于类和结构体 - 它只取决于对象的实际使用情况。区别在于,由于结构体变量传递数据本身(而不是引用),它们更有可能满足堆栈分配的条件。(但请记住,整个过程取决于实现,因为堆栈和堆本身实现细节。) - Theodoros Chatzigiannakis
1
@Stochastically 还有一些 unsafe 块,您可以在其中使用 stackalloc 并对分配发生的位置进行显式控制(但仅适用于结构实例,而不是类实例)。虽然这不是一个非常干净的解决方案,特别是如果它妨碍了代码的语义,这就是为什么我将其作为注释的原因。尽管如此,作为最后的手段,它可能会对您有所帮助。 - Theodoros Chatzigiannakis
显示剩余4条评论
3个回答

6
如果你有内存分配瓶颈问题,你应该:
1. 使用 "对象池" (如 @MartinJames 所说)。在应用程序启动时初始化对象池。对象池应该提高堆分配的性能。
2. 使用结构体 (或任何值类型) 作为本地变量,因为栈分配比堆分配快得多。
3. 避免隐式内存分配。例如,当您添加项到 List<> 时:
如果 Count 已经等于 Capacity,则会通过自动重新分配内部数组来增加 List 的容量,并在添加新元素之前将现有元素复制到新数组中 (来源 MSDN)。
4. 避免装箱。这非常昂贵:
与简单赋值相比,装箱和拆箱是计算上昂贵的过程。当一个值类型被装箱时,必须分配和构造一个新对象。在较小的程度上,拆箱所需的强制转换也是计算上昂贵的。(来源 MSDN)
5. 避免捕获变量的 lambda 表达式(因为会为捕获的变量创建新对象)

1

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

我想,在C#中,您可以使用BlockingCollection。用大量T预先填充它,并从中获取对象,使用它们,然后使用Add()返回。

对于数量众多且较大的对象(例如服务器数据缓冲区)或具有复杂和冗长的构造函数/析构函数(例如http接收器/解析器组件),此方法非常有效- 将这些对象(因为在NET中本质上是指针)从队列中弹出/推入要比不断创建它们,然后稍后由GC销毁它们快得多。

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


谢谢@MartinJames,事实上我已经在我的一些代码中实现了这种想法,但突然想到如果能够强制C#在不同情况下使用不同的内存堆,那将会更加方便。 - Stochastically
一些便利可以内置 :) 我所有的池化对象都派生自一个基类,该基类具有“Pool myPool”私有成员和一个无参数的“release()”方法,将对象推回池中 - 无需传递池实例并且不会将对象错误地推回到其他池中。私有堆的问题是,正如您所知道的,它涉及线程间通信:如果将从私有池分配的对象排队到另一个线程,则需要池锁来删除它,从而破坏了私有堆的优势。 - Martin James

0

这不是特别的C#或.NET问题。为了使CPU核心运行得最优,它需要所有数据都在CPU缓存中。如果某个数据不在CPU缓存中,就会发生缓存故障,CPU会处于空闲状态,直到数据从内存中获取到缓存中。

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

CLR进行堆分配的方式对于CPU缓存来说更加优化。通过自己处理内存分配,很难达到相同的性能。


我不想自己处理分配。我只想能够指示C#拥有多个内存堆,这样当我创建新对象时,不同的线程就不需要互相等待。 - Stochastically

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