多线程C ++程序的性能不佳

时间:2013-03-02 18:11:25

标签: c++ multithreading

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

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

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

这种问题是否有任何一般原因?

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

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

编辑第3部分:感谢所有建议。我正在进行一些单元测试(看哪些部件正在减速),正如一些人所建议的那样。由于程序需要一段时间才能加载和执行,因此需要花时间查看测试中的任何结果,因此我对迟到的响应表示道歉。我认为我想澄清的主要观点是线程可能导致程序运行缓慢的可能原因。从我从评论中收集的内容来看,它根本就不应该。当我找到合理的解决方案时,我会发帖,再次感谢。

(最终)编辑第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;
}

9 个答案:

答案 0 :(得分:26)

如果你有足够的CPU内核,并且还有很多工作要做,那么多线程运行的时间不应超过单线程模式 - 实际的CPU时间可能要长一些,但是“挂钟时间” “应该更短。我很确定你的代码有一些瓶颈,其中一个线程阻塞另一个线程。

这是因为有一件或多件事 - 我先列出它们,然后详细说明:

  1. 线程中的某些锁定阻止第二个线程运行。
  2. 在线程之间共享数据(真或“假”共享)
  3. 缓存捶打。
  4. 争夺一些导致颠簸和/或阻塞的外部资源。
  5. 一般设计错误的代码......
  6. 线程中的某些锁定阻止第二个线程运行。

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

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

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

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

    如果两个线程使用[并经常更新它的值]相同的变量,则两个线程必须交换“我已更新此”消息,并且CPU必须先从其他CPU获取数据它可以继续使用变量。由于“数据”在“每个缓存行”级别上共享,并且缓存行通常为32个字节,如下所示:

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

    将被归类为称为“虚假共享”的东西 - 更新的ACTUAL数据对于每个CPU是唯一的,但由于数据在相同的32字节区域内,因此核心仍将更新相同的内存。

    缓存抖动。

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

    某些外部资源的竞争导致颠簸和/或阻塞。

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

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

    一般设计不好

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

    工作单元太小,因此所有时间都花在启动和停止线程上,而且没有做足够的工作。例如,你说小的数字是“计算这是否是一个素数”每个线程,一次一个数字,它可能需要更长的时间来给线程的数字而不是计算“是这个实际上是一个素数“ - 解决方案是给每个线程提供一组数字(可能是10,20,32,64等),然后一次性报告整个批次的结果。

    还有很多其他“糟糕的设计”。如果不理解你的代码,就很难肯定。

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

答案 1 :(得分:4)

阅读CPU Caches and Why You Care以了解为什么从一个线程到多个线程的算法的简单端口通常会导致性能大大降低和负面可扩展性。为并行性设计的特定算法可以解决过度交互操作,错误共享和其他缓存污染的原因。

答案 2 :(得分:4)

以下是您可能需要考虑的一些事项。

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

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

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

4°)在其他答案中已经提到了一些缓存问题。根据经验,虚假分享可能会像你观察到的那样伤害表演。我的最后一条建议(应该是我的第一篇)是使用分析器工具,例如Linux Perf或OProfile。如果您遇到这种性能下降,原因肯定会非常明显。

答案 3 :(得分:2)

其他答案都涉及可能导致症状的一般指导原则。我会给出我自己的,希望不是多余的版本。然后我将谈谈如何在讨论所有内容的情况下找到问题的根源。

一般来说,有几个原因可以让你预期多个线程的表现会更好:

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

上面列举的主要原因是,您希望多个线程执行得不太好,都是基于资源争用:

  • 磁盘争用:已经详细解释,可能是一个可能的问题,特别是如果您一次编写小缓冲区而不是批处理
  • 如果将线程安排到同一核心上,则会产生CPU时间争用:如果您正在设置关联,则可能不是您的问题。但是,您仍应仔细检查
  • 缓存颠簸:如果你有亲和力,同样可能不是你的问题,但如果这是你的问题,这可能会非常昂贵。
  • 共享内存:再次详细讨论并且似乎不是您的问题,但审核代码以查看它不会有什么坏处。
  • NUMA:再次谈到。如果您的工作线程固定到不同的核心,您将需要检查它需要访问的工作是否是主核心的本地工作。

好到目前为止并没有太多新意。它可以是上述任何一种或不同的。问题是,对于您的情况,您如何才能发现额外时间的来源。有几个策略:

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

我对你的案子的直觉:

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

答案 4 :(得分:2)

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

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

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

如果您发现需要帮助来解释探查器输出,请随意将其添加到您的问题中。

答案 5 :(得分:1)

后面的正确痛苦可以追踪为什么线程没有按计划运行。人们可以分析地这样做,或者可以使用工具来显示正在发生的事情。我已经在ftrace中取得了很好的成绩,Linux是Linux的dtrace的克隆版(它反过来基于VxWorks,Greenhill的Integrity操作系统和Mercury计算机系统公司在一段时间内所做的事情。)

我发现此页面非常有用:http://www.omappedia.com/wiki/Installing_and_Using_Ftrace,尤其是thisthis部分。不要担心它是一个OMAP导向的网站;我在X86 Linux上使用它就好了(尽管你可能需要构建一个包含它的内核)。还要记住,GTKWave查看器主要用于查看来自VHDL开发的日志跟踪,这就是它看起来“奇怪”的原因。只是有人意识到它对于sched_switch数据也是一个可用的查看器,并且保存了它们写一个。

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

答案 6 :(得分:0)

如果您使用1个线程变慢,可能是由于使用线程安全库函数或线程设置的开销。为每个作业创建一个线程会导致很大的开销,但可能没有你提到的那么多。 换句话说,它可能是某些线程安全库函数的一些开销。

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

有些库非常讨厌线程安全开销。例如,许多rand()实现使用全局锁,而不是使用线程本地prgn。这种锁定开销远大于生成数字,并且在没有分析器的情况下很难跟踪。

减速也可能源于你所做的微小变化,例如声明变量volatile,这通常不是必需的。

答案 7 :(得分:0)

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

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

答案 8 :(得分:0)

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