我有一个数据结构(向量),元素必须由函数解析,其中元素可以由不同的线程解析。
以下是解析方法:
void ConsumerPool::parse(size_t n_threads, size_t id)
{
for (size_t idx = id; idx < nodes.size(); idx += n_threads)
{
// parse node
//parse(nodes[idx]);
parse(idx);
}
}
其中:
n_threads
是线程总数id
是当前线程的(单义)索引并创建线程如下:
std::vector<std::thread> threads;
for (size_t i = 0; i < n_threads; i++)
threads.emplace_back(&ConsumerPool::parse, this, n_threads, i);
不幸的是,即使此方法有效,如果线程数太高,我的应用程序的性能也会降低。我想了解为什么即使这些线程之间没有同步,性能也会降低。
以下是根据使用的线程数经过的时间(线程开始和最后一次join()返回之间):
创建线程所需的时间始终在1/2 ms之间。 该软件已使用其发布版本进行了测试。以下是我的配置:
2x Intel(R) Xeon(R) CPU E5507 @ 2.27GHz
Maximum speed: 2.26 GHz
Sockets: 2
Cores: 8
Logical processors: 8
Virtualization: Enabled
L1 cache: 512 KB
L2 cache: 2.0 MB
L3 cache: 8.0 MB
修改
parse()
函数的作用如下:
// data shared between threads (around 300k elements)
std::vector<std::unique_ptr<Foo>> vfoo;
std::vector<rapidxml::xml_node<>*> nodes;
std::vector<std::string> layers;
void parse(int idx)
{
auto p = vfoo[idx];
// p->parse() allocate memory according to the content of the XML node
if (!p->parse(nodes[idx], layers))
vfoo[idx].reset();
}
答案 0 :(得分:7)
您使用的处理器英特尔(R)Xeon(R)CPU E5507只有4个内核(请参阅http://ark.intel.com/products/37100/Intel-Xeon-Processor-E5507-4M-Cache-2_26-GHz-4_80-GTs-Intel-QPI)。 因此,拥有超过4的线程会导致速度变慢,因为从您提供的数据中可以看到上下文切换。
您可以在以下链接中详细了解上下文切换:https://en.wikipedia.org/wiki/Context_switch
答案 1 :(得分:3)
尝试解析线程内元素的连续范围,例如变化
for (size_t idx = id; idx < nodes.size(); idx += n_threads)
{
// parse node
parse(nodes[idx]);
}
到
for (size_t idx = id * nodes.size()/n_threads; idx < (id+1)*nodes.size()/n_threads; idx++)
{
// parse node
parse(nodes[idx]);
}
这应该更适合缓存。
最好预先计算size = (id+1)*nodes.size()/n_threads
并在循环停止条件下使用它,而不是在每次迭代时计算它。
答案 2 :(得分:3)
更新
我们仍然没有关于parse()
的内存访问模式的大量信息,以及从内存中读取输入数据花费的时间与写入/读取私有临时内存所花费的时间之间的关系
你说p->parse()
&#34;根据XML节点的内容分配内存&#34;。如果它再次释放它,你可能会看到在每个线程中保持足够大的暂存缓冲区的大加速。内存分配/释放是一个全球性的&#34;需要线程之间同步的事情。线程感知分配器有望通过满足 线程释放的内存中的分配来处理分配/释放/分配/免费模式,因此在私有L1或L2缓存中它可能仍然很热在那个核心上。
使用某种分析来查找真正的热点。它可能是内存分配/释放,也可能是读取内存的代码。
您的双插槽Nehalem Xeon没有超线程,因此如果非HT感知操作系统在两个逻辑核心上调度两个逻辑核心,则无法解决线程相互减慢的问题。相同的物理核心。
您应该使用性能计数器(例如Linux perf stat
或英特尔的VTune)进行调查,一旦传递4个线程,您是否会在每个线程中获得更多缓存未命中。 Nehalem使用大型共享(对于整个套接字)L3(也称为最后一级)缓存,因此在同一套接字上运行的更多线程会对此产生更大的压力。相关的性能事件将类似于LLC_something,IIRC。
你一定要看看L1 / L2未命中,看看这些是如何根据线程数进行扩展的,以及如何通过对node[]
的跨步访问和连续访问进行更改。
您可以检查其他性能计数器以查找错误共享(一个线程的私有变量与另一个线程的私有变量共享一个缓存行,因此缓存行在核心之间反弹)。真的只是寻找随线程数变化的任何perf事件;这可能指明解释的方式。
像2插槽Nehalem这样的多插槽系统将具有NUMA (Non-uniform_memory_access)。一个支持NUMA的操作系统会尝试为进行分配的核心分配快速的内存。
所以假设你的缓冲区的所有物理页面都连接到你的两个插槽中的一个。在这种情况下,它可能不是你可以或应该避免的东西,因为我假设你在将它交给多个线程进行解析之前以单线程方式填充数组。但是,一般情况下,尝试在最常用的线程中分配内存(尤其是临时缓冲区),方便的时候。
这可能部分解释了线程数量不完美的扩展。虽然这很可能与事情无关,但@ AntonMalyshev的回答并没有帮助。让每个线程在一个连续的范围内工作,而不是以n_threads
的步幅跨越数组,对于L2 / L1缓存效率应该更好。
node[]
是一个指针向量(因此,对于8个线程,每个线程仅使用它在node[]
中触及的每个64字节高速缓存行的8个字节)。但是,每个线程可能会触及指向数据结构和字符串中的更多内存。如果node
条目指向其他数据结构和字符串中的单调递增位置,则对node[]
的跨步访问会为线程触及的大多数内存创建非连续访问模式。
跨步访问模式的一个可能的好处: Strided意味着如果所有线程以或多或少相同的速度运行,它们都会同时查看内存的相同部分。强大>领先的线程将从L3未命中减速,而其他线程因为看到L3命中而迎头赶上。 (除非发生某些事情导致一个线程过于落后,比如操作系统在一个时间片内对其进行调度。)
因此,与有效使用每核L2 / L1相比,L3与RAM带宽/延迟相比更为重要。也许使用更多线程,L3带宽无法跟上来自多个核心的L2缓存的相同缓存行的所有请求。 (L3不足以足够快地同时满足来自所有核心的恒定L2未命中,即使它们都在L3中命中。)
仅当node[]
的连续范围指向其他内存的连续范围时,此参数才适用于node[]
指向的所有内容。
答案 3 :(得分:2)
对于CPU绑定进程,添加超出可用核心数量的其他线程将降低整体性能。减少是由于调度和其他内核交互。对于这种情况,最佳线程数通常是核数-1。剩余的核心将由内核和其他正在运行的进程使用。
我在这里更详细地讨论了这个主题A case for minimal multithreading
仔细观察硬件和数字,我怀疑你正在进行超线程争用。对于4核cpu,使用超线程模拟8个核。对于完全cpu绑定的进程,超线程实际上会降低性能。这里有一些有趣的讨论Hyper-threading和详细信息Wikipedia hyper-threading
答案 4 :(得分:2)
2个CPU(每个4核) 线程在共享内存空间中运行。性能下降是由于在CPU之间移动共享内存(线程无法直接访问不同CPU中的缓存,更多线程=&gt;更多移动=&gt;更大的性能下降)。