OpenMP 如何实现对关键部分的访问

How does OpenMP implement access to critical sections?

本文关键字:键部 访问 何实现 实现 OpenMP      更新时间:2023-10-16

我想读取一个输入文件(C/C++)并尽快独立处理每一行。处理本身需要一些时间,所以我决定使用 OpenMP 线程。我有这个代码:

#pragma omp parallel num_threads(num_threads)
{
  string line;
  while (true) {
#pragma omp critical(input)
    {
      getline(f, line);
    }
    if (f.eof())
      break;
    process_line(line);
  }
}

我的问题是,如何确定要使用的最佳线程数?理想情况下,我希望在运行时动态检测这一点。我不明白parallel的动态时间表选项,所以我不能说这是否有帮助。有什么见解吗?

另外,我不确定如何"手动"确定最佳数字。我为我的特定应用程序尝试了各种数字。我本以为top报告的 CPU 使用率会有所帮助,但它没有(!就我而言,CPU 使用率始终保持在 num_threads*(85-95) 左右。但是,使用 pv 来观察我读取输入的速度,我注意到最佳数字约为 2-5;在此之上,输入速度变小。所以我的问题是 - 为什么在使用 850 个线程时会看到 10 的 CPU 使用率?这可能是由于 OpenMP 处理等待进入关键部分的线程的方式效率低下吗?

编辑:这里有一些时间。我通过以下方式获得它们:

for NCPU in $(seq 1 20) ; do echo "NCPU=$NCPU" ; { pv -f -a my_input.gz | pigz -d -p 20 | { { sleep 60 ; PID=$(ps gx -o pid,comm | grep my_prog | sed "s/^ *//" | cut -d " " -f 1) ; USAGE=$(ps h -o "%cpu" $PID) ; kill -9 $PID ; sleep 1 ; echo "usage: $USAGE" >&2 ; } & cat ; } | ./my_prog -N $NCPU >/dev/null 2>/dev/null ; sleep 2 ; } 2>&1 | grep -v Killed ; done

NCPU=1[8.27兆字节/秒]用法:98.4

NCPU=2[12.5兆字节/秒]用法:196

NCPU=3[18.4兆字节/秒]用法:294

NCPU=4[23.6兆字节/秒]用法:393

NCPU=5[28.9兆字节/秒]用法:491

NCPU=6[33.7兆字节/秒]用法:589

NCPU=7[37.4兆字节/秒]用法:688

NCPU=8[40.3兆字节/秒]用法:785

NCPU=9[41.9兆字节/秒]用法:884

NCPU=10[41.3兆字节/秒]用法:979

NCPU=11[41.5兆字节/秒]用法:1077

NCPU=12[42.5兆字节/秒]用法:1176

NCPU=13[41.6兆字节/秒]用法:1272

NCPU=14[42.6兆字节/秒]用法:1370

NCPU=15[41.8兆字节/秒]用法:1493

NCPU=16[40.7兆字节/秒]用法:1593

NCPU=17[40.8兆字节/秒]用法:1662

NCPU=18[39.3兆字节/秒]用法:1763

NCPU=19[38.9兆字节/秒]用法:1857

NCPU=20[37.7兆字节/秒]用法:1957

我的问题是,我可以在 40 CPU 使用率下达到 785MB/s,但也可以达到 1662 CPU 使用率。这些额外的周期去哪儿了??

编辑2:感谢Lirik和John Dibling,我现在明白,我发现上述时间令人困惑的原因与I/O无关,而是与OpenMP实现关键部分的方式有关。我的直觉是,如果你在 CS 中有 1 个线程,10 个线程等待进入,那么当第一个线程退出 CS 时,内核应该唤醒另一个线程并让它进入。时间表明并非如此:可能是线程自行唤醒多次并发现CS被占用了吗?这是线程库还是内核的问题?

"我想读取一个输入文件(C/C++),并尽快独立处理每一行。

从文件读取会使应用程序受到 I/O 限制,因此仅读取部分所能达到的最大性能是以最大磁盘速度读取(在我的计算机上,CPU 时间不到 10%)。这意味着,如果您能够从任何处理中完全释放读取线程,则需要处理花费少于剩余 CPU 时间(我的计算机为 90%)。如果行处理线程占用的长度超过剩余的CPU时间,那么您将无法跟上硬盘驱动器的步伐。

在这种情况下,有几种选择:

  1. 将输入排队,让处理线程取消排队"工作",直到它们赶上呈现的输入(假设您有足够的 RAM 来这样做)。
  2. 打开足够的线程并最大化处理器,直到读取所有数据,这是您尽力而为的方案。
  3. 限制读取/处理,以便您不会占用所有系统资源(以防您担心 UI 响应能力和/或用户体验)。

"...处理本身需要一些时间,所以我决定使用 OpenMP 线程"

这是一个好兆头,但这也意味着您的 CPU 利用率不会很高。这是您可以优化性能的部分,正如John Dibling所提到的,最好手动完成。通常,最好将每一行排队,让处理线程从队列中提取处理请求,直到您没有其他要处理的内容。后者也被称为生产者/消费者设计模式 - 并发计算中非常常见的模式。

更新

为什么两者之间有区别

  • (i) 每个进程获取锁定、拉取数据、释放锁定、过程数据;以及
  • (ii) 一个进程:拉取数据、获取锁、排队块、释放锁、
  • 其他:获取锁定、取消排队块、释放锁定、处理数据?

差别很小:在某种程度上,两者都代表了消费者/生产者的模式。在第一种情况下 (i) 您没有实际的队列,但您可以将文件流视为您的生产者(队列),而使用者是从流中读取的线程。在第二种情况 (ii) 中,您显式实现了使用者/生产者模式,该模式更健壮、可重用,并为生产者提供了更好的抽象。如果您决定使用多个"输入通道",那么后一种情况更好。

最后(也可能是最重要的),您可以使用具有单个生产者和单个使用者的无锁队列,这将使 (ii) 在让你受到 I/O 约束方面比 (i) 快得多。使用无锁队列,您可以在不锁定的情况下拉取数据排队块dequque 块

您能做的最好的事情就是通过重复的测量-调整-比较循环手动调整它。

用于处理数据集的最佳线程数高度依赖于许多因素,其中最重要的是:

  1. 数据本身
  2. 你用来处理它的算法
  3. 运行线程的 CPU
  4. 操作系统

您可以尝试设计某种启发式方法来测量处理器的吞吐量并动态调整它,但这种事情往往比它的价值更麻烦。

通常,对于受 I/O 限制的任务,我通常从每个内核大约 12 个线程开始,然后从那里进行调整。 对于 CPU 密集型任务,我将从每个内核大约 4 个线程开始,然后从那里开始。 关键是"从那里开始"部分,如果你真的想优化你的处理器使用。

还要记住,如果您真的想要优化,则应使此设置可配置,因为部署的每个系统将具有不同的特征。