5

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

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

我尝试了几件事,包括pthread_setaffinity_np将线程设置为单个 CPU(在我正在使用的服务器上可用的 24 个 CPU 中),以及pthread_setschedparam设置调度策略(我只尝试了 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;
}
4

9 回答 9

33

如果你有足够多的 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 等),然后一次性报告整个批次的结果。

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

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

于 2013-03-02T18:49:42.940 回答
6

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

于 2013-03-02T18:29:10.513 回答
4

以下是您可能想要研究的几件事。

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

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

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

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

于 2013-03-06T02:17:52.107 回答
2

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

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

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

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

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

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

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

我对您的情况的直觉:

  • 您的图形结构对您的工作核心不是 NUMA 友好的。
  • 内核实际上可以将您的工作线程调度到亲和核心之外。如果您没有为要固定的核心打开 isolcpu,则可能会发生这种情况。
于 2013-03-07T23:19:33.283 回答
2

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

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

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

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

于 2013-03-09T05:09:57.457 回答
1

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

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

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

于 2013-03-09T07:42:25.970 回答
0

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

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

于 2013-03-11T17:06:54.183 回答
0

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

于 2013-03-11T19:35:03.880 回答
0

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

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

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

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

于 2013-03-10T17:03:52.070 回答