【问题标题】:Poor performance in multi-threaded C++ program在多线程 C++ 程序中性能不佳
【发布时间】:2013-03-02 18:11:25
【问题描述】:

我有一个在 Linux 上运行的 C++ 程序,其中创建了一个新线程来独立于主线程执行一些计算量大的工作(计算工作通过将结果写入文件来完成,这些文件最终非常大)。但是,我的表现相对较差。

如果我直接实现程序(不引入其他线程),它会在大约 2 小时内完成任务。使用多线程程序大约需要 12 个小时才能完成相同的任务(这是在仅生成一个线程的情况下进行测试的)。

我尝试了几件事,包括 pthread_setaffinity_np 将线程设置为单个 CPU(在我正在使用的服务器上可用的 24 个 CPU 中),以及 pthread_setschedparam 设置调度策略(我只尝试了 SCHED_BATCH)。但到目前为止,这些影响可以忽略不计。

这类问题有什么一般原因吗?

编辑:我添加了一些我正在使用的示例代码,希望这是最相关的部分。函数 process_job() 是实际执行计算工作的,但在这里包含太多。基本上,它读取两个数据文件,并使用这些数据对内存中的图形数据库执行查询,其中结果在几个小时内写入两个大文件。

编辑第 2 部分:澄清一下,问题不在于我想使用线程来提高我所拥有的算法的性能。但相反,我想同时运行我的算法的许多实例。因此,我希望算法在放入线程时运行速度与我根本不使用多线程时相似。

编辑第 3 部分:感谢所有建议。正如一些人建议的那样,我目前正在做一些单元测试(查看哪些部分正在减慢)。由于该程序需要一段时间来加载和执行,因此需要时间来查看测试的任何结果,因此对于迟到的回复我深表歉意。我认为我想澄清的要点是线程可能导致程序运行缓慢的可能原因。根据我从 cmets 收集到的信息,根本不应该。当我找到合理的解决方案时,我会发布,再次感谢。

(最终)编辑第 4 部分:事实证明,问题毕竟与线程无关。在这一点上描述它会太麻烦(包括使用编译器优化级别),但这里发布的想法非常有用和赞赏。

struct sched_param sched_param = {
    sched_get_priority_min(SCHED_BATCH)
};

int set_thread_to_core(const long tid, const int &core_id) {
   cpu_set_t mask;
   CPU_ZERO(&mask);
   CPU_SET(core_id, &mask);
   return pthread_setaffinity_np(tid, sizeof(mask), &mask);
}

void *worker_thread(void *arg) {
   job_data *temp = (job_data *)arg;  // get the information for the task passed in
   ...
   long tid = pthread_self();
   int set_thread = set_thread_to_core(tid, slot_id);  // assume slot_id is 1 (it is in the test case I run)
   sched_get_priority_min(SCHED_BATCH);
   pthread_setschedparam(tid, SCHED_BATCH, &sched_param);
   int success = process_job(...);  // this is where all the work actually happens
   pthread_exit(NULL);
}

int main(int argc, char* argv[]) {
   ...
   pthread_t temp;
   pthread_create(&temp, NULL, worker_thread, (void *) &jobs[i]);  // jobs is a vector of a class type containing information for the task
   ...
   return 0;
}

【问题讨论】:

  • 如果不知道代码的要点,这真的很难说。主线程是否同时在做任何事情? (如果不是,多线程就是浪费。如果它做磁盘访问,那真的会损害性能。)
  • 我测试了生成单个线程来完成任务。好的,我将尝试重构我的代码并发布相关部分。
  • @minitech:好吧,不一定是浪费,如果它负责 UI 或其他东西......(虽然我想它不是 OP 的情况)
  • @minitech 是的,这是真的。虽然,对于我所做的测试,我只调度了一个线程来完成任务(只完成了一次写入),并且花了 12 个小时。
  • 您是否尝试过对线程代码路径的不同部分进行计时? Something 比较慢,但你不知道是什么。尝试消除算法的不同部分。例如,不要将输出写入磁盘,而是将其丢弃(在单线程和多线程版本中)。

标签: c++ multithreading


【解决方案1】:

如果您有足够多的 CPU 内核,并且有大量工作要做,那么在多线程模式下运行应该不会比单线程模式花费更长的时间 - 实际的 CPU 时间可能会长一点,但是“挂钟时间” "应该更短。我很确定您的代码存在某种瓶颈,其中一个线程阻塞了另一个线程。

这是因为其中一个或多个原因 - 我将首先列出它们,然后在下面详细介绍:

  1. 线程中的某些锁阻止了第二个线程的运行。
  2. 线程之间共享数据(真或“假”共享)
  3. 缓存抖动。
  4. 对某些外部资源的竞争导致抖动和/或阻塞。
  5. 一般来说代码设计不佳...

线程中的某些锁正在阻止第二个线程运行。

如果有一个线程需要一个锁,而另一个线程想要使用被这个线程锁定的资源,它就必须等待。这显然意味着线程没有做任何有用的事情。锁定应该保持在最低限度,只需要锁定一小段时间。使用一些代码来识别锁是否持有您的代码,例如:

while (!tryLock(some_some_lock))
{
    tried_locking_failed[lock_id][thread_id]++;
}
total_locks[some_lock]++;

打印一些锁的统计信息将有助于确定锁存在争议的位置 - 或者您可以尝试“在调试器中按下 break 并查看您所在位置”的旧技巧 - 如果线程一直在等待某个锁,那么这就是阻碍进步的原因......

线程间数据共享(真或“假”共享)

如果两个线程使用[并经常更新它的值]同一个变量,那么两个线程将不得不交换“我已经更新了这个”消息,并且 CPU 必须先从另一个 CPU 获取数据它可以继续使用变量。由于“数据”在“每个缓存行”级别上共享,并且缓存行通常为 32 字节,类似于:

int var[NUM_THREADS]; 
...
var[thread_id]++; 

会被归类为“虚假共享”——更新的实际数据对于每个 CPU 都是唯一的,但由于数据位于相同的 32 字节区域内,内核仍将更新相同的内存。

缓存抖动。

如果两个线程进行大量内存读写,CPU 的缓存可能会不断地丢弃好的数据来填充另一个线程的数据。有一些技术可用于确保两个线程不会在 CPU 使用的缓存部分“锁步”中运行。如果数据是 2^n(2 的幂)并且相当大(缓存大小的倍数),则为每个线程“添加偏移量”是个好主意 - 例如 1KB 或 2KB。这样,当第二个线程将相同的距离读入数据区域时,它不会完全覆盖第一个线程当前正在使用的缓存区域。

对某些外部资源的竞争导致抖动和/或阻塞。

如果两个线程正在读取或写入硬盘、网卡或其他一些共享资源,这可能导致一个线程阻塞另一个线程,这反过来意味着性能下降。代码也可能检测到不同的线程并进行一些额外的刷新,以确保在开始与另一个线程工作之前以正确的顺序或类似的顺序写入数据。

在处理资源(用户模式库或内核模式驱动程序)的代码中也可能存在内部锁,当多个线程使用同一资源时会阻塞。

一般设计不好

这是“许多其他可能出错的事情”的“总括”。如果需要一个线程中的一个计算结果来推进另一个,显然,在那个线程中不能完成很多工作。

工作单元太小,所以所有时间都花在启动和停止线程上,而没有完成足够的工作。例如,假设您向每个线程分配小数字以“计算这是否是质数”,一次一个数字,将数字提供给线程可能比计算“这是实际上是一个素数” - 解决方案是为每个线程提供一组数字(可能是 10、20、32、64 等),然后一次性报告整个批次的结果。

还有很多其他“糟糕的设计”。如果不了解您的代码,很难确定。

您的问题完全有可能不是我在这里提到的问题,但很可能是其中之一。希望这个答案有助于确定原因。

【讨论】:

  • 请注意,缓存行现在通常是 64 字节,而不是 32 字节。
【解决方案2】:

阅读CPU Caches and Why You Care 以了解为什么将算法从一个线程移植到多个线程往往会导致性能大大降低和负面可扩展性。 专门为并行设计的算法负责处理过度活跃的联锁操作、错误共享和其他缓存污染原因。

【讨论】:

    【解决方案3】:

    这里有一些你可能想研究的事情。

    1°) 您是否在工作线程和主线程之间输入了任何临界区(锁、信号量等)? (如果您的查询修改了图表,则应该是这种情况)。如果是这样,那可能是多线程开销的来源之一:竞争锁的线程通常会降低性能。

    2°)您使用的是 24 核机器,我假设它是 NUMA(非统一内存访问)。由于您在测试期间设置了线程关联性,因此您应该密切注意硬件的内存拓扑。查看 /sys/devices/system/cpu/cpuX/ 中的文件可以帮助您解决这个问题(注意 cpu0 和 cpu1 不一定靠得很近,因此不一定共享内存)。大量使用内存的线程应该使用本地内存(分配在与它们正在执行的内核相同的 NUMA 节点中)。

    3°) 您正在大量使用磁盘 I/O。那是哪种 I/O?如果每个线程每次都执行一些同步 I/O,您可能需要考虑异步系统调用,以便操作系统负责将这些请求调度到磁盘。

    4°)其他答案中已经提到了一些缓存问题。从经验来看,虚假分享可能会像您观察到的那样严重损害表演。我的最后一个建议(这应该是我的第一个)是使用分析器工具,例如 Linux Perf 或 OProfile。由于您正在经历这样的性能下降,原因肯定会非常清楚。

    【讨论】:

      【解决方案4】:

      其他答案都解决了可能导致您出现症状的一般准则。我会给出我自己的,希望不是过度冗余的版本。然后我会谈谈如何在考虑到所有讨论的情况下找到问题的根源。

      一般来说,您希望多线程性能更好有几个原因:

      • 一项工作依赖于某些资源(磁盘、内存、缓存等),而其他部分可以独立于这些资源或所述工作负载进行。
      • 您有多个 CPU 内核可以并行处理您的工作负载。

      上面列举的主要原因是,您认为多个线程表现不佳都是基于资源争用:

      • 磁盘争用:已详细解释,可能是一个问题,尤其是在一次写入小缓冲区而不是批处理时
      • 如果线程被调度到同一个核心,CPU 时间争用:如果您设置关联,可能不是您的问题。但是,您仍应仔细检查
      • 缓存抖动:同样,如果您有亲和力,这可能不是您的问题,但如果是您的问题,这可能会非常昂贵。
      • 共享内存:再次详细讨论,似乎不是您的问题,但审核代码以检查它不会有什么坏处。
      • NUMA:再次谈到。如果您的工作线程被固定到不同的核心,您将需要检查它需要访问的工作是否在主核心本地。

      好的,到目前为止还没有多少新东西。它可以是上述任何一种,也可以不是。问题是,对于您的情况,您如何检测额外时间的来源。有一些策略:

      • 审核代码并寻找明显的区域。不要花太多时间来做这件事,因为如果您一开始就编写程序,这通常是徒劳的。
      • 重构单线程代码和多线程代码以隔离一个 process() 函数,然后在关键检查点进行分析以尝试解决差异。然后缩小范围。
      • 将资源访问重构为批次,然后在对照和实验上对每个批次进行分析以说明差异。这不仅会告诉您需要将精力集中在哪些领域(磁盘访问、内存访问和花费时间),而且进行此重构甚至可能会提高您的整体运行时间。例子:
        • 首先将图形结构复制到线程本地内存(在单线程情况下执行直接复制)
        • 然后执行查询
        • 然后设置异步写入磁盘
      • 尝试找到具有相同症状的最小可重现工作负载。这意味着更改您的算法以执行它已经执行的操作的子集。
      • 确保系统中没有其他可能导致差异的噪音(如果其他用户在工作核心上运行类似系统)。

      我对你的情况的直觉:

      • 您的图形结构对您的工作核心不是 NUMA 友好的。
      • 内核实际上可以将您的工作线程调度到亲和内核之外。如果您没有为要固定到的核心启用 isolcpu,则可能会发生这种情况。

      【讨论】:

        【解决方案5】:

        我不能告诉你你的程序出了什么问题,因为你没有分享足够的内容来进行详细的分析。

        我可以告诉你的是,如果这是我的问题,我首先会尝试在我的应用程序上运行两个分析器会话,一个在单线程版本上,另一个在双线程配置上。探查器报告应该让您很好地了解额外时间的去向。请注意,您可能不需要对整个应用程序运行进行分析,具体取决于问题,在您分析几秒钟或几分钟后,时间差异可能会变得明显。

        就 Linux 的分析器选择而言,您可能需要考虑 oprofile 或作为第二选择 gprof

        如果您发现在解释分析器输出时需要帮助,请随时将其添加到您的问题中。

        【讨论】:

          【解决方案6】:

          追查线程未按计划工作的原因可能是一件很痛苦的事。可以分析地这样做,或者可以使用工具来显示正在发生的事情。 ftrace 是 Linux 对 Solaris 的 dtrace 的克隆(它又基于 VxWorks、Greenhill 的 Integrity OS 和 Mercury Computer Systems Inc 长期以来一直在做的事情。)

          我发现这个页面特别有用:http://www.omappedia.com/wiki/Installing_and_Using_Ftrace,尤其是thisthis 部分。不要担心它是一个面向 OMAP 的网站;我已经在 X86 Linux 上很好地使用了它(尽管您可能必须构建一个内核来包含它)。还要记住,GTKWave 查看器主要用于查看 VHDL 开发的日志跟踪,这就是它看起来“奇怪”的原因。只是有人意识到它也是 sched_switch 数据的可用查看器,这让他们免于编写一个。

          使用 sched_switch 跟踪器,您可以查看线程何时(但不一定是为什么)运行,这可能足以为您提供线索。 “为什么”可以通过仔细检查其他一些示踪剂来揭示。

          【讨论】:

            【解决方案7】:

            如果您因使用 1 个线程而变慢,这可能是由于使用线程安全库函数或线程设置的开销所致。为每个作业创建一个线程会导致显着的开销,但可能没有您所指的那么多。 换句话说,这可能是一些线程安全库函数的一些开销。

            最好的办法是分析您的代码以找出时间花在哪里。如果它在库调用中,请尝试找到替换库或自己实现它。如果瓶颈是线程创建/销毁,请尝试重用线程,例如在 C++11 中使用 OpenMP 任务或 std::async。

            有些库的线程安全开销真的很讨厌。例如,许多 rand() 实现使用全局锁,而不是使用线程本地 prgn。这种锁定开销比生成数字要大得多,并且如果没有分析器就很难跟踪。

            减速也可能源于您所做的小改动,例如声明变量 volatile,这通常是不必要的。

            【讨论】:

              【解决方案8】:

              我怀疑您正在一台带有一个单核处理器的机器上运行。这个问题在那种系统上是不可并行的。您的代码一直在使用处理器,它有固定数量的周期提供给它。它实际上运行得更慢,因为额外的线程会为问题增加昂贵的上下文切换。

              在单处理器机器上能够很好地并行化的唯一问题是那些允许一个执行路径运行而另一个执行路径被阻塞等待 I/O,以及允许一个执行路径的情况(例如保持响应式 GUI)线程获得一些处理器时间比尽快执行代码更重要。

              【讨论】:

                【解决方案9】:

                如果您只想运行算法的许多独立实例,您可以将多个作业(具有不同参数,可以由单个脚本处理)提交到您的集群吗?这将消除分析和调试多线程程序的需要。我在多线程编程方面没有太多经验,但是如果您使用 MPI 或 OpenMP,那么您也必须为簿记编写更少的代码。例如,如果需要一些通用的初始化例程并且进程可以在之后独立运行,您可以通过在一个线程中初始化并进行广播来做到这一点。无需维护锁等。

                【讨论】:

                  猜你喜欢
                  • 2011-03-30
                  • 1970-01-01
                  • 2018-06-13
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 2011-05-01
                  • 1970-01-01
                  • 1970-01-01
                  相关资源
                  最近更新 更多