1

我想读取一个输入文件(在 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);
  }
}

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

另外,我不确定如何“手动”确定最佳数量。我为我的特定应用尝试了各种数字。我原以为报告的 CPU 使用率top会有所帮助,但它没有(!)在我的情况下,CPU 使用率始终保持在 num_threads*(85-95) 左右。但是,pv为了观察我读取输入的速度,我注意到最佳数字在 2-5 左右;高于此,输入速度变小。所以我的问题是 - 为什么在使用 10 个线程时我会看到 850 的 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.27MB/s] 使用率:98.4

NCPU=2 [12.5MB/s] 使用率:196

NCPU=3 [18.4MB/s] 使用:294

NCPU=4 [23.6MB/s] 使用率:393

NCPU=5 [28.9MB/s] 使用:491

NCPU=6 [33.7MB/s] 使用:589

NCPU=7 [37.4MB/s] 使用率:688

NCPU=8 [40.3MB/s] 使用率:785

NCPU=9 [41.9MB/s] 使用:884

NCPU=10 [41.3MB/s] 使用率:979

NCPU=11 [41.5MB/s] 使用率:1077

NCPU=12 [42.5MB/s] 使用率:1176

NCPU=13 [41.6MB/s] 使用率:1272

NCPU=14 [42.6MB/s] 使用率:1370

NCPU=15 [41.8MB/s] 使用:1493

NCPU=16 [40.7MB/s] 使用:1593

NCPU=17 [40.8MB/s] 使用率:1662

NCPU=18 [39.3MB/s] 使用率:1763

NCPU=19 [38.9MB/s] 使用率:1857

NCPU=20 [37.7MB/s] 使用:1957

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

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

4

2 回答 2

2

“我想读取一个输入文件(在 C/C++ 中)并尽可能快地独立处理每一行。”

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

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

  1. 将输入排队并让处理线程出队“工作”,直到它们赶上所呈现的输入(假设您有足够的 RAM 来执行此操作)。
  2. 打开足够多的线程并最大化您的处理器,直到读取所有数据,这是您最好的方案。
  3. 限制读取/处理,以免占用所有系统资源(以防您担心 UI 响应和/或用户体验)。

“......处理本身需要一些滴答声,所以我决定使用 OpenMP 线程”

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

更新

为什么会有区别

  • (i) 每个进程获取锁、拉取数据、释放锁、处理数据;和
  • (ii) 一个过程:拉数据,获取锁,入队块,释放锁,
  • 其他:获取锁、出队块、释放锁、处理数据?

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

最后(可能也是最重要的),您可以使用具有单个生产者和单个消费者的无锁队列,这将使 (ii) 比 (i) 更快地让您成为 i/o 绑定。使用无锁队列,您可以在没有锁定的情况下提取数据将块 队和出队列。

于 2012-06-06T21:54:20.687 回答
1

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

用于处理数据集的最佳线程数在很大程度上取决于许多因素,其中最重要的是:

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

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

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

另外请记住,如果您真的想优化,您应该使此设置可配置,因为部署此设置的每个系统都具有不同的特征。

于 2012-06-06T21:34:05.890 回答