性能随着线程数量的增加而降低(无同步)

Nic*_*ick 7 c++ performance multithreading

我有一个数据结构(向量),元素必须由函数解析,其中元素可以由不同的线程解析.

以下是解析方法:

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

哪里:

  • 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);
Run Code Online (Sandbox Code Playgroud)

不幸的是,即使这种方法有效,如果线程数太高,我的应用程序的性能也会降低.我想了解为什么即使这些线程之间没有同步,性能也会降低.

以下是根据使用的线程数经过的时间(线程开始和最后一次join()返回之间):

  • 2个主题:500毫秒
  • 3个主题:385毫秒
  • 4个主题:360毫秒
  • 5个主题:475毫秒
  • 6个主题:580毫秒
  • 7个主题:635毫秒
  • 8个主题:660毫秒

创建线程所需的时间始终在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
Run Code Online (Sandbox Code Playgroud)

编辑:

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();
}
Run Code Online (Sandbox Code Playgroud)

Nem*_*vic 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


Pet*_*des 3

更新:

我们仍然没有很多关于 的内存访问模式的信息parse(),以及它花费多少时间从内存读取输入数据与花费多少时间写入/读取私有暂存存储器。

你说p->parse()“根据XML节点的内容分配内存”。如果它再次释放它,您可能会发现由于在每个线程中分配了足够大的暂存缓冲区,因此速度会大大提高。内存分配/释放是一个“全局”的事情,需要线程之间的同步。线程感知分配器有望通过满足线程刚刚释放的内存的分配来处理分配/释放/分配/释放模式,因此该核心上的私有 L1 或 L2 缓存中的它可能仍然很热。

使用某种分析来找到真正的热点。它可能是内存分配/释放,也可能是读取某些内存的代码。


您的双插槽 Nehalem Xeon 不具备超线程功能,因此,如果非 HT 感知操作系统在同一物理核心的两个逻辑核心上调度两个线程,您就不会遇到线程相互减慢的问题。


您应该使用性能计数器(例如 Linuxperf stat或 Intel 的 VTune)调查一旦超过 4 个线程,每个线程是否会出现更多缓存未命中。Nehalem 使用大型共享(对于整个套接字)L3(也称为最后一级)缓存,因此在同一套接字上运行的更多线程会产生更大的压力。相关的性能事件将类似于 LLC_something、IIRC。

您绝对应该查看 L1/L2 misses,并了解它们如何随线程数量变化,以及跨步访问与连续访问如何变化node[]

您可以检查其他性能计数器以查找错误共享(一个线程的私有变量与另一个线程的私有变量共享缓存行,因此缓存行在核心之间反弹)。实际上只是寻找随线程数量变化的任何性能事件;这可能会为解释指明方向。


像 2 插槽 Nehalem 这样的多插槽系统将具有 NUMA (Non-uniform_memory_access)。具有 NUMA 意识的操作系统将尝试为执行分配的核心分配快速的内存。

因此,想必您的缓冲区将内存中的所有物理页附加到两个套接字之一。在这种情况下,您可能无法或不应该避免这种情况,因为我假设您在将数组交给多个线程进行解析之前以单线程方式填充数组。不过,一般来说,如果方便的话,请尝试在最常使用内存的线程中分配内存(尤其是暂存缓冲区)。

这可能部分解释了线程数量扩展不那么完美的原因。尽管这更可能与事情无关,但如果 @AntonMalyshev 的回答没有帮助。让每个线程在连续的范围内工作,而不是以 的步长跨过数组n_threads,应该会更好地提高 L2 / L1 缓存效率。

node[]是一个指针向量(因此对于 8 个线程,每个线程仅使用它所接触的每个 64 字节缓存行的 8 个字节node[])。然而,每个线程可能会触及指向的数据结构和字符串中更多的内存。如果node条目指向其他数据结构和字符串中单调递增的位置,则跨步访问会对node[]线程触及的大部分内存创建非连续访问模式。


跨步访问模式的一个可能的好处是:跨步意味着如果所有线程以大致相同的速度运行,它们都会同时查看内存的同一部分。 领先的线程会因 L3 未命中而减慢速度,而其他线程会赶上,因为它们看到 L3 命中。(除非发生一些事情让一个线程落后太多,比如操作系统在一个时间片上取消了它的调度。)

因此,也许 L3 与 RAM 带宽/延迟比有效使用每核 L2/L1 更重要。也许线程较多时,L3 带宽无法跟上多个内核的 L2 缓存对相同缓存行的所有请求。(L3 的速度不够快,无法同时满足所有核心的持续 L2 未命中,即使它们都在 L3 中命中。)

node[]仅当连续范围指向其他内存的连续范围时,此参数才适用于 所指向的所有内容node[]