【发布时间】:2011-02-23 14:00:26
【问题描述】:
我有一个包含许多独立计算的程序,所以我决定将它并行化。
我使用 Parallel.For/Each。
结果对于双核机器来说还可以——大部分时间 CPU 利用率约为 80%-90%。 然而,使用双 Xeon 机器(即 8 核)我只能得到大约 30%-40% 的 CPU 利用率,尽管程序在并行部分上花费了相当多的时间(有时超过 10 秒),而且我看到它采用与串行部分相比,这些部分中的线程数增加了大约 20-30 个。每个线程需要超过 1 秒才能完成,所以我认为它们没有理由不并行工作 - 除非存在同步问题。
我用了VS2010自带的profiler,结果很奇怪。 尽管我只在一个地方使用了锁,但分析器报告说,大约 85% 的程序时间都花在了同步上(还有 5-7% 的睡眠,5-7% 的执行,低于 1% 的 IO)。
锁定的代码只是一个缓存(字典)get/add:
bool esn_found;
lock (lock_load_esn)
esn_found = cache.TryGetValue(st, out esn);
if(!esn_found)
{
esn = pData.esa_inv_idx.esa[term_idx];
esn.populate(pData.esa_inv_idx.datafile);
lock (lock_load_esn)
{
if (!cache.ContainsKey(st))
cache.Add(st, esn);
}
}
lock_load_esn 是 Object 类型的类的静态成员。esn.populate 为每个线程使用单独的 StreamReader 从文件中读取数据。
但是,当我按下同步按钮查看导致最大延迟的原因时,我看到分析器报告的行是函数入口行,而不是报告锁定的部分本身。
它甚至不报告包含上述代码的函数(提醒 - 程序中唯一的 lock)作为噪声级别为 2% 的阻塞配置文件的一部分。噪音水平为 0% 时,它会报告程序的所有功能,我不明白为什么它们会被视为阻塞同步。
所以我的问题是 - 这里发生了什么?
怎么会有 85% 的时间花在同步上?
如何找出我的程序并行部分的真正问题?
谢谢。
更新:深入了解线程后(使用非常有用的可视化工具),我发现大部分同步时间都花在等待 GC 线程完成内存分配上,而且频繁由于通用数据结构调整大小操作,因此需要分配。
我将不得不看看如何初始化我的数据结构,以便它们在初始化时分配足够的内存,可能避免这种 GC 线程的竞争。
我会在今天晚些时候报告结果。
更新:看来内存分配确实是问题的原因。当我为并行执行类中的所有字典和列表使用初始容量时,同步问题更小。我现在只有大约 80% 的同步时间,CPU 利用率达到峰值 70%(之前的峰值只有大约 40%)。
我更深入地研究了每个线程,发现现在对 GC allocate 的许多调用都是为了分配不属于大字典一部分的小对象。
我通过为每个线程提供一个预先分配的此类对象池解决了这个问题,我使用它而不是调用“new”函数。
所以我基本上为每个线程实现了一个单独的内存池,但是以一种非常粗糙的方式,这非常耗时而且实际上不是很好 - 我仍然需要使用很多 new对于这些对象的初始化,我现在只在全局范围内执行一次,并且 GC 线程上的争用较少,即使必须增加池的大小也是如此。
但这绝对不是我喜欢的解决方案,因为它不容易泛化,我不想编写自己的内存管理器。
有没有办法告诉 .NET 为每个线程分配预定义的内存量,然后从本地池中获取所有内存分配?
【问题讨论】:
-
“所以我认为他们没有理由并行工作” - 你错过了一个“不”吗?
-
为了完整起见,lock_load_esn和cache是什么类型?而且缓存是静态成员,对吧?
-
糟糕,忘记了“不”,谢谢。 lock_load_esn 是 Object(即 static Object lock_load_esn = new object() ),而 cache 是 Dictionary
包装器,它的作用并不比 Dictionary 对 TryGetValue/ContainsKey/Add 方法所做的更多。跨度>
标签: c# visual-studio-2010 profiling parallel-processing