使用Visual Studio Concurrency Visualizer我现在明白为什么切换到 Parallel.For 没有任何好处:只有 9% 的时间机器忙于执行代码,其余的时间为 71% 的同步和 17% 的内存管理(1).
检查下图中的所有橙色条纹,我发现总是涉及 GC (2)。
阅读完所有这些有趣的主题后...
.. 我是否正确地假设所有这些线程都需要使用单个内存管理对象,因此消除了在堆上分配对象的需要,我的场景将大大改善?比如使用结构体而不是类,数组而不是动态列表等等?
我需要做很多工作才能朝这个方向弯曲我的代码。只是想在开始之前确定一下。
最佳答案
从您的屏幕截图来看,内存分配似乎在等待 GC 完成时被阻止。有服务器和工作站GC模式,并且可以是并发的,也可以是非并发的,但所有选项都需要阻塞线程至少一段时间。我会更详细地检查您在 GC 上花费的频率和时间,以及 gen 0/1 和 2 运行的频率。
我相信每个线程都有一个单独的临时段用于分配,因此不需要同步分配,除非它需要一个新段,或者分配位于大对象堆上。但我找不到这方面的引用。
无论如何,您可能会从减少分配的数量和大小中受益。如果可能的话,使用对象池或内存池来重用内存。您还可能会受益于增加内存量并检查应用程序是否存在内存泄漏。对于内存的一般建议是应该有两种类型的分配:
- 仅存活很短时间的小型临时分配,例如在方法调用期间存活的临时对象。
- 在“应用程序”期间有效的任意大小的长期分配。
如果遵循这种模式,几乎所有垃圾都应该在 Gen 0/1 中收集,而第 2 代收集应该相当罕见。
如果您要分配许多小对象或大块内存,这也有点取决于。如果是前者,您可以考虑使用结构,因为它们是堆栈分配的。如果后者还需要考虑内存碎片,这也应该通过使用仅分配固定大小的内存块的内存池来改进。
编辑:
最简单的对象池可能是这样的:
public class ObjectPool<T>
{
private ConcurrentBag<T> pool = new ConcurrentBag<T>();
public T Get(Func<T> constructor) => pool.TryTake(out var result) ? result : constructor();
public void Return(T obj) => pool.Add(obj);
}
这假设对象代表相同的资源,例如某个固定大小的字节数组。但也有现有的实现:
关于c# - TPL 和内存管理,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/64256760/