我有一个包含许多独立计算的程序,因此我决定对其进行并行化。
我使用 Parallel.For/Each。
对于双核机器来说,结果还不错——大多数时候 CPU 利用率约为 80%-90%。
然而,对于双 Xeon 机器(即 8 核),我只能获得大约 30%-40% 的 CPU 利用率,尽管该程序在并行部分花费了相当多的时间(有时超过 10 秒),而且我发现它使用了与串行部分相比,这些部分中的线程大约多 20-30 个。每个线程需要超过 1 秒才能完成,因此我认为它们没有理由不并行工作 - 除非存在同步问题。
我使用VS2010的内置分析器,结果很奇怪。
即使我只在一处使用锁,探查器报告大约 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%的时间都花在了同步上呢?
我如何找出程序并行部分的真正问题是什么?
Thanks.
Update:深入研究线程(使用非常有用的可视化工具)后,我发现大部分同步时间都花在等待 GC 线程完成内存分配上,并且由于通用数据结构调整大小操作,需要频繁的分配。
我必须了解如何初始化我的数据结构,以便它们在初始化时分配足够的内存,从而可能避免 GC 线程的竞争。
我将在今天晚些时候报告结果。
Update:看来内存分配确实是问题的原因。当我对并行执行的类中的所有字典和列表使用初始容量时,同步问题较小。我现在只有大约 80% 的同步时间,CPU 利用率峰值为 70%(之前的峰值仅为 40% 左右)。
我进一步深入研究每个线程,发现现在对 GC allocate 的许多调用都是为了分配不属于大字典的小对象。
我通过为每个线程提供一个预先分配的此类对象池解决了这个问题,我使用它而不是调用“新”函数。
所以我本质上为每个线程实现了一个单独的内存池,但是以一种非常粗暴的方式,这是非常耗时的,而且实际上不是很好 - 我仍然需要使用很多new对于这些对象的初始化,现在我只在全局执行一次,并且 GC 线程上的争用更少,即使在必须增加池的大小时也是如此。
但这绝对不是我喜欢的解决方案,因为它不容易推广,而且我不想编写自己的内存管理器。
有没有办法告诉 .NET 为每个线程分配预定义的内存量,然后从本地池中获取所有内存分配?