使用 OpenMP 任务的生产者-消费者

2024-01-04

我正在尝试使用 OpenMP 中的任务来实现并行算法。 并行编程模式基于生产者-消费者的思想,但是 由于消费者进程比生产者进程慢,我想使用一些 生产者和若干消费者。 主要思想是创建与生产者一样多的操作系统线程,然后每个 这些将创建要并行完成的任务(由消费者)。每一个 生产者将与一定数量的消费者相关联(即 检查者数/搜索者数)。 我在每个芯片有 6 个内核的英特尔双芯片服务器上运行该算法。 问题是,当我只使用一个生产者(搜索者)并且数量不断增加时 消费者(检查者)的数量随着消费者(检查者)数量的增加,性能下降得非常快 即使核心数量正确,消费者也会增长(见下表) 100% 工作。 另一方面,如果我增加生产者的数量,平均时间 减少或至少保持稳定,即使有一定比例的数量 消费者。 在我看来,所有的改进都是通过输入的划分来实现的 在生产者中,任务只是窃听。但话又说回来,我没有任何 对一位生产者的行为的解释。我是否遗漏了一些东西 OpenMP-任务逻辑?难道我做错了什么?

-------------------------------------------------------------------------
|   producers   |   consumers   |   time        |
-------------------------------------------------------------------------
|       1       |       1       |   0.642935    |
|       1       |       2       |   3.004023    |
|       1       |       3       |   5.332524    |
|       1       |       4       |   7.222009    |
|       1       |       5       |   9.472093    |
|       1       |       6       |   10.372389   |
|       1       |       7       |   12.671839   |
|       1       |       8       |   14.631013   |
|       1       |       9       |   14.500603   |
|       1       |      10       |   18.034931   |
|       1       |      11       |   17.835978   |
-------------------------------------------------------------------------
|       2       |       2       |   0.357881    |
|       2       |       4       |   0.361383    |
|       2       |       6       |   0.362556    |
|       2       |       8       |   0.359722    |
|       2       |      10       |   0.358816    |
-------------------------------------------------------------------------

我的代码的主要部分如下:

int main( int argc, char** argv) {

  // ... process the input (read from file, etc...)

  const char *buffer_start[numSeekers];
  int buffer_len[numSeekers];

  //populate these arrays dividing the input
  //I need to do this because I need to overlap the buffers for
  //correctness, so I simple parallel-for it's not enough 

  //Here is where I create the producers
  int num = 0;
  #pragma omp parallel for num_threads(numSeekers) reduction(+:num)
  for (int i = 0; i < numSeekers; i++) {
      num += seek(buffer_start[i], buffer_len[i]);
  }

  return (int*)num;
}

int seek(const char* buffer, int n){

  int num = 0;

  //asign the same number of consumers for each producer 
  #pragma omp parallel num_threads(numCheckers/numSeekers) shared(num)
  {
    //only one time for every producer
    #pragma omp single
    {
      for(int pos = 0; pos < n; pos += STEP){
    if (condition(buffer[pos])){
      #pragma omp task shared(num)
      {
        //check() is a sequential function
        num += check(buffer[pos]);
      }
    }
      }
      #pragma omp taskwait
    }
  return num;
}

观察到的行为是由于您没有嵌套的事实parallel启用的区域。发生的情况是,在第一种情况下,您实际上正在经历 OpenMP 任务的巨大开销。这很可能是因为check()与 OpenMP 运行时引入的开销相比,它没有做足够的工作。为什么 1 和 2 个生产者的表现会如此?

当仅与一个生产者一起运行时,外部parallel区域仅使用一个线程执行。这样的parallel地区是inactive根据 OpenMP API 规范,它们只是串行执行内部代码(唯一的开销是额外的函数调用和通过指针访问共享变量)。在这种情况下,内部parallel区域,虽然在嵌套并行性被禁用时被嵌套,但变为active并激发了很多任务。任务会带来相对较高的开销,并且这种开销随着线程数量的增加而增加。与 1 个消费者的内部parallel地区也是inactive因此串行运行,没有任务开销。

当与两个生产者一起运行时,外部parallel地区是active因而内在parallel区域被渲染inactive(记住 - 没有启用嵌套并行性),因此,根本不会创建任何任务 -seek()只是串行运行。没有任务开销,代码的运行速度几乎是 1 个生产者/1 个消费者情况的两倍。运行时间不依赖于消费者的数量,因为内部parallel地区始终是inactive,无论指定了多少个线程。

任务分配和对共享变量的协调访问会带来多大的开销?我创建了一个简单的综合基准测试,它执行以下代码:

for (int i = 0; i < 10000000; i++) {
   ssum += sin(i*0.001);
}

在默认优化级别为 GCC 4.7.2 的 Westmere CPU 上,串行执行时间不到一秒。然后我使用简单的方法介绍了任务atomic构造以保护对共享变量的访问ssum:

#pragma omp parallel
{
   #pragma omp single
   for (int i = 0; i < 10000000; i++) {
      #pragma omp task
      {
         #pragma omp atomic
         ssum += sin(i*0.001);
      }
   }
}

(不需要taskwait这里因为在隐式屏障末尾有一个调度点parallel region)

我还创建了一个更复杂的变体,它按照 Massimiliano 提出的相同方式执行缩减:

#define STRIDE 8

#pragma omp parallel
{
   #pragma omp single
   for (int i = 0; i < 10000000; i++) {
      #pragma omp task
      {
         const int idx = omp_get_thread_num();
         ssumt[idx*STRIDE] += sin(i*0.001);
      }
   }
   #pragma omp taskwait

   const int idx = omp_get_thread_num();
   #pragma omp atomic
   ssum += ssumt[idx*STRIDE];
}

代码是用 GCC 4.7.2 编译的,如下所示:

g++ -fopenmp -o test.exe test.cc

在双插槽 Westmere 系统(总共 12 个核心)上以批处理模式运行它(因此没有其他进程可以干预),并且具有不同数量的线程和插槽上不同的线程放置,可以获得两个代码的以下运行时间:

Configuration   ATOMIC       Reduction    ATOMIC slowdown
2 + 0            2,79 ±0,15   2,74 ±0,19   1,8%
1 + 1            2,72 ±0,21   2,51 ±0,22   8,4% <-----
6 + 0           10,14 ±0,01  10,12 ±0,01   0,2%
3 + 3           22,60 ±0,24  22,69 ±0,33  -0,4%
6 + 6           37,85 ±0,67  38,90 ±0,89  -2,7%

(运行时间以秒为单位给出,测量方法为omp_get_wtime(),平均超过 10 次运行/标准。还显示了偏差/;x + y in the Configuration列表示x第一个套接字上的线程和y第二个插座上的螺纹)

正如您所看到的,任务的开销是巨大的。它比使用的开销高得多atomic而不是对线程私有累加器应用归约。此外,分配部分atomic with +=编译为锁定的比较和交换指令(LOCK CMPXCHG) - 开销并不比调用高多少omp_get_thread_num()每一次。

还应该注意的是,双插槽 Westmere 系统是 NUMA,因为每个 CPU 都有自己的内存,并且对另一个 CPU 内存的访问要通过 QPI 链路,因此延迟会增加(并且可能会降低带宽)。作为ssum变量是共享的atomic在这种情况下,在第二个处理器上运行的线程本质上是在发出远程请求。尽管如此,两个代码之间的差异可以忽略不计(除了标记的双线程情况 - 我必须调查原因)并且atomic当线程数量增加时,代码的性能甚至开始优于减少后的代码。

在多尺度 NUMA 系统上,同步atomic这种方法可能会成为更大的负担,因为它会增加本来就较慢的远程访问的锁定开销。我们的 BCS 耦合节点之一就是这样的系统之一。 BCS (Bull Coherence Switch) 是 Bull 的专有解决方案,它使用 XQPI (eXternal QPI) 将多个 Nehalem-EX 板连接到一个系统中,从而引入三个级别的 NUMA(本地内存;同一板上的远程内存) ;远程板上的远程存储器)。当在一个这样的系统上运行时,该系统由 4 个板组成,每个板有 4 个八核 Nehalem-EX CPU(总共 128 个内核),atomic可执行文件运行 1036 秒(!!),而缩减方法运行 1047 秒,即两者仍然执行大约相同的时间(我之前的声明是atomic方法慢了 21.5%,这是由于测量期间操作系统服务抖动所致)。这两个数字均来自单次运行,因此不太具有代表性。请注意,在此系统上,XQPI 链路为板间 QPI 消息引入了非常高的延迟,因此锁定的成本非常高,但不是that昂贵的。可以通过使用归约来消除部分开销,但必须正确实施。首先,归约变量的本地副本也应该位于线程执行的 NUMA 节点的本地副本,其次,应该找到一种方法来不调用omp_get_thread_num()。这两个可以通过许多不同的方式来实现,但最简单的一种就是使用threadprivate变量:

static double ssumt;
#pragma omp threadprivate(ssumt)

#pragma omp parallel
{
   ssumt = 0.0;

   #pragma omp single
   for (int i = 0; i < 10000000; i++) {
      #pragma omp task
      {
         ssumt += sin(i*0.001);
      }
   }
   #pragma omp taskwait

   #pragma omp atomic
   ssum += ssumt;
}

进入ssumt不需要保护,因为两个任务很少在同一线程中同时执行(必须进一步调查这是否符合 OpenMP 规范)。此版本的代码执行了 972 秒。同样,这与 1036 秒相差不远,并且仅来自一次测量(即,它可能只是统计波动),但从理论上讲,它应该更快。

带回家的教训:

  • 阅读有关嵌套的 OpenMP 规范parallel地区。通过设置环境变量来启用嵌套并行性OMP_NESTED to true或通过致电omp_set_nested(1);。如果启用,活动嵌套的级别可以由OMP_MAX_ACTIVE_LEVELS正如马西米利亚诺所指出的。
  • 留意数据争用,并尝试使用最简单的方法来防止它们。并非每次使用更复杂的方法都能带来更好的性能。
  • 特殊系统通常需要特殊编程。
  • 如有疑问,请使用螺纹检查工具(如果有)。 Intel 有一个(商业),Oracle 的 Solaris Studio(以前称为 Sun Studio)也有一个(免费;尽管产品名称有 Linux 版本)。
  • 注意开销!尝试将作业分割成足够大的块,以便创建数百万个任务所产生的开销不会抵消所获得的并行增益。
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

使用 OpenMP 任务的生产者-消费者 的相关文章

  • 更快的算法来计算有多少数字可以被范围内的特定整数整除

    int a b c d 0 cin gt gt a gt gt b gt gt c for int i a i lt b i if i c 0 d cout lt
  • 没有 Unicode 字节顺序标记。无法切换到 Unicode

    我正在使用 XSD 编写 XML 验证器 下面是我所做的 但是当验证器到达该线时while list Read 它给了我错误 没有 Unicode 字节顺序标记 无法切换到 Unicode 有人可以帮我解决吗 public class Va
  • 为什么派生类不使用基类的operator=(赋值运算符)?

    以下是实际问题的简化版本 而不是打电话Base operator int 代码似乎生成了一个临时的Derived对象并复制它 既然函数签名似乎完美匹配 为什么不使用基本赋值运算符 这个简化的示例没有显示任何不良影响 但原始代码在析构函数中有
  • F10键没被抓住

    I have a Windows Form and there overriden ProcessCmdKey However this works with all of the F Keys except for F10 I am tr
  • .net Framework (.net 4.0) 中定义 Base 3 数字的类

    我正在寻找一些可以用来定义 3 基数 三进制数 的类 有什么我可以在 net 框架中使用的东西或者我需要写一些东西吗 谢谢你的帮助 您可以使用解析Convert ToInt32 s base http msdn microsoft com
  • 抽象类或接口。哪种方式是正确的?

    有两种方法可以选择抽象类或接口 微软解决方案和Oracle解决方案 微软 设计指南 请使用抽象 在 Visual Basic 中为 MustInherit 类而不是接口来将协定与实现分离 http msdn microsoft com en
  • 指示泛型返回动态类型的对象

    这个问题是我原来问题的后续问题here https stackoverflow com questions 2541184 using a type object to create a generic 假设我有以下泛型类 简化 class
  • Microsoft.Graph - 如何从具有不同用户名的共享邮箱发送?

    我目前正在将使用 SMTP 的服务代码移植到 Office 365 通过 SMTP 我可以使用 发件人 字段在来自共享收件箱的邮件上设置不同的用户名 同时保留共享电子邮箱地址 这似乎无法通过 Office 365 运行 其工艺流程为 客户填
  • 数据损坏 C++ 和 Python 之间的管道

    我正在编写一些代码 从 Python 获取二进制数据 将其通过管道传输到 C 对数据进行一些处理 在本例中计算互信息度量 然后将结果通过管道传输回 Python 在测试时 我发现如果我发送的数据是一组尺寸小于 1500 X 1500 的 2
  • 不要声明只读可变引用类型 - 为什么不呢?

    我一直在阅读这个问题 https stackoverflow com questions 2274412 immutable readonly reference types fxcop violation do not declare r
  • C# 中处理 SQL 死锁的模式?

    我正在用 C 编写一个访问 SQL Server 2005 数据库的应用程序 该应用程序是数据库密集型的 即使我尝试优化所有访问 设置适当的索引等 我预计迟早会遇到死锁 我知道为什么会发生数据库死锁 但我怀疑我能否在某个时候发布不发生死锁的
  • 我们可以有虚假中断吗?

    我正在创建一个任务轮询器 每分钟都会查找任务 它看起来像这样 public class Poller private final ExecutorService e Executors newSingleThreadExecutor pub
  • 为什么WCF中不允许方法重载?

    假设这是一个ServiceContract ServiceContract public interface MyService OperationContract int Sum int x int y OperationContract
  • Dynamics Crm:获取状态代码/状态代码映射的元数据

    在 Dynamics CRM 2011 中 在事件实体上 状态原因 选项集 也称为状态代码 与 状态 选项集 也称为状态代码 相关 例如看这个截图 当我使用 API 检索状态原因选项集时 如下所示 RetrieveAttributeRequ
  • C++ 标准中短语“构造函数没有名称”的含义

    在尝试理解 C 标准中的 构造函数没有名称 这句话时 我似乎在 clang 中发现了一个错误 有人可以证实这一点吗 VS2015 and gcc rejects this code and I think they it are is co
  • 微软语音识别速度

    我正在使用微软的语音识别器开发一个小型练习应用程序 对于我正在做的事情来说 我似乎无法让它足够快地识别单个单词 我希望能够正常说话 系统将从我所说的内容中抓取 关键字 并生成一个字符串 目前我正在使用 5 个单词的自定义语法 红 蓝 黄 绿
  • 你能解释一下这个C++删除问题吗?

    我有以下代码 std string F WideString ws GetMyWideString std string ret StringUtils ConvertWideStringToUTF8 ws ret return ret W
  • c# 替代方案中 cfusion_encrypt 中填充的密钥是什么?

    我找到了从这里复制 C 中的 cfusion encrypt 函数的答案 ColdFusion cfusion encrypt 和 cfusion decrypt C 替代方案 https stackoverflow com questio
  • 创建带有部分的选项卡式侧边栏 WPF

    我正在尝试创建一个带有部分的选项卡式侧边栏 如 WPF 中的以下内容 我考虑过几种方法 但是有没有更简单 更优雅的方法呢 方法一 列表框 Using a ListBox并将 SelectedItem 绑定到右侧内容控件所绑定的值 为了区分标
  • 使用剪贴板 SetText 换行

    如何使用 SetText 方法添加换行符 I tried Clipboard SetText eee n xxxx 但当我将剪贴板数据粘贴到记事本中时 它没有给我预期的结果 预期结果 eee xxxx 我怎样才能做到这一点 Windows

随机推荐