🥑Cpp多核开发和并行计算CUDA_OpenMP_OpenCL

关键词

C++ | GPU | 线程 | 并发 | 几何分解 | 并行结构 | 分布式内存 | 堆栈 | 曼德尔布罗分形集 | OpenMPI | 扩散限制聚集 | 模拟 | 数据消息加密解密 | 工作负载 | 排序

🏈page指点迷津 | Brief

🎯要点

  1. 线程和并发C++:🎯创建多线程求数据平均值方法:使用几何分解,使用functor创建线程,使用functor资源获取即初始化(RAII)原则创建管理线程。🎯避免线程间争用条件:使用mutex求解数值平均值。🎯共享资源和程序终止方式:生产者-消费者代码示例,使用semaphore通过共享缓冲区同步两种类型的线程,使用固定迭代数或消息通知终止程序。🎯线程和进程同步:理发店问题代码示例。🎯读写共同资源:读者和作者代码示例。🎯内存排序:银行账户代码示例,排队吸烟代码示例,修改生产者-消费者示例为监视器内的缓冲区操作,修改读者和作者示例为优先权。🎯异步线程:使用“后台”线程的平均值查找示例。🎯动态和静态线程:计算曼德尔布罗分形集代码示例。

  2. 并行数据结构C++:🎯以监视器为模型的并发堆栈示例,允许在压入和弹出线程之间发送信号的并发堆栈示例。使用细粒度锁定的并发队列示例。🎯使用粗粒度同步的并行列表示例,插入、删除粗粒度并行列表。🎯用于细粒度锁定的节点结构,列表容器的插入和擦除方法,使用延迟同步的列表容器的节点结构。🎯无锁堆栈示例,内置自定义节点内存管理的无锁堆栈。🎯链表节点移除示例。

  3. 分布式内存编程C++,使用OpenMPI示例:🎯多程序、多数据(MPMD)。🎯缓冲通信。🎯矩阵向量乘法。🎯蝴蝶模式,使用点对点通信来实现全对全的收集操作。🎯桶排序的并行实现,使用集体通信。🎯使用 MPI_Pack/MPI_Unpack 传递像素结构实例。🎯一个程序将进程分成两组,根据组创建两个通信器,并在两组中的每组内执行广播。🎯一个程序,根据 MPI_COMM_WORLD 的等级是偶数还是奇数将 MPI_COMM_WORLD 分成两个内部通信器,广播在两个通信器中的每一个内执行。🎯并行存储桶排序实现,使用 RMA(远程内存访问) 在进程之间进行数据交换。🎯通过 MPI I 大致相等的整数类型数据块分布的示例。🎯通过 MPI 循环块分布数据示例。🎯MPI 程序生成线程示例。🎯修改矩阵乘法示例,以便 Scalasca 检测 MMpartial 函数循环。🎯基于 Boost.MPI 的主从设置加速数据交换。🎯Boost.MPI 中的归约示例, 缩小运算产生两个质量的重心。🎯应用案例1:扩散限制聚集(晶体形成过程)模拟:由移动粒子和晶体填充的二维细胞网格进行二维模拟。🎯应用案例2:使用 MPI 执行数据消息加密解密。🎯应用案例3:MPI主从模式布置,划分曼德尔布罗分形集计算工作负载。

  4. CUDA ,OpenMP,OpenCL

C++OpenMP并行计算

OpenMP 是一个在共享内存多处理环境中进行并行编程的库。 它是一种应用程序编程接口 (API),允许开发人员编写可以利用单台计算机内多个内核和处理器的计算能力的程序。 该库得到编译器(gcc、clang、msvc)的良好支持,并且是可移植的,可以在 Linux、macOS 和 Windows 上运行。

最重要的是,它的 API 足够简单和直观,您可以专注于程序的并行设计,而不是陷入并行编程实现的细节中。 使用 OpenMP,您可以简单地向代码中添加一些编译器指令,指示代码的哪些部分应该并行执行,而库会处理其余的事情。 这显着减少了编写并行程序所需的工作量和时间,使其成为希望在不牺牲代码可读性和可维护性的情况下提高应用程序性能的开发人员的必备工具。

那么,让我们从一个简单的例子开始。 最简单的示例是一个简单的 for 循环,它单独执行一个表达式。 考虑以下估计 π 值的函数:

 double estimate_pi_seq(uint64_t num_steps) {
     auto delta = 1.0 / static_cast<double>(num_steps);
     double pi = 0.0;
     for (uint64_t step = 0; step < num_steps; ++step) {
         auto x = delta * (static_cast<double>(step) + 0.5);
         pi += 4.0 / (1 + x * x);
     }
     return pi * delta;
 }

该函数通过函数 4 /\left(1+x^{\wedge} 2\right) 从 0 到 1 的数值积分来估计 \pi 的值。使用大 num_steps 评估函数可以很好地逼近 \pi。

现在,让我们使用 OpenMP 并行化该函数。鉴于循环内的每个计算都是独立的,我们可以轻松地使用parallel-for 并行化代码。在 OpenMP 中,可以写为

 double estimate_pi_par(uint64_t num_steps) {
     auto delta = 1.0 / static_cast<double>(num_steps);
     double pi = 0.0;
 #pragma omp parallel for default(none) shared(num_steps, delta) reduction(+:pi)
     for (uint64_t step = 0; step < num_steps; ++step) {
         auto x = delta * (static_cast<double>(step) + 0.5);
         pi += 4.0 / (1 + x * x);
     }
     return pi * delta;
 }

也许更容易显示两者之间的差异:

 --- seq
 +++ par
 @@ -1,6 +1,7 @@
 -double estimate_pi_seq(uint64_t num_steps) {
 +double estimate_pi_par(uint64_t num_steps) {
      auto delta = 1.0 / static_cast<double>(num_steps);
      double pi = 0.0;
 +#pragma omp parallel for default(none) shared(num_steps, delta) reduction(+:pi)
      for (uint64_t step = 0; step < num_steps; ++step) {
          auto x = delta * (static_cast<double>(step) + 0.5);
          pi += 4.0 / (1 + x * x);

本质上,我们用一行并行化代码。 就是这样。 此行告诉编译器使用 OpenMP 并并行化 for 循环。 这里,shared()显式指定哪些变量是跨线程共享的,reduction()指定要从部分结果中减少哪个变量。 所有其他未指定的变量都假定为私有的,即只能由其自己的线程访问。 通过这个单行代码,编译器将为我们完成所有艰苦的工作并生成高效的并行实现。

我们甚至不需要担心低级实现来解决 \pi 变量的竞争条件 - 我们明确地将其指定为归约变量,并且 OpenMP 根据我们的规范来处理它。 请注意,OpenMP 不是一根魔杖。 它不会找出哪些变量造成竞争条件。 程序员有责任了解哪些变量会引起竞争条件,并需要明确要求 OpenMP 来处理它。

 #include <iostream>
 #include <omp.h>
 #include <string>
 ​
 using namespace std::string_literals;
 ​
 double estimate_pi_seq(uint64_t num_steps) {
     auto delta = 1.0 / static_cast<double>(num_steps);
     double pi = 0.0;
     for (uint64_t step = 0; step < num_steps; ++step) {
         auto x = delta * (static_cast<double>(step) + 0.5);
         pi += 4.0 / (1 + x * x);
     }
     return pi * delta;
 }
 ​
 double estimate_pi_par(uint64_t num_steps) {
     auto delta = 1.0 / static_cast<double>(num_steps);
     double pi = 0.0;
 #pragma omp parallel for default(none) shared(num_steps, delta) reduction(+:pi)
     for (uint64_t step = 0; step < num_steps; ++step) {
         auto x = delta * (static_cast<double>(step) + 0.5);
         pi += 4.0 / (1 + x * x);
     }
     return pi * delta;
 }
 ​
 int main(int argc, const char **argv) {
     uint64_t num_steps = std::stoull(argv[1]);
     auto calc_pi = [argv](uint64_t num_steps) {
         return "seq"s == argv[2] ? estimate_pi_seq(num_steps) : estimate_pi_par(num_steps);
     };
     auto pi = calc_pi(num_steps);
     std::cout << pi << "\n";
 }

要编译,只需向编译器指定 -fopenmp 标志即可。

 $ g++ -std=c++17 -O3 -fopenmp pi.cc -o pi
 ​
 $ time ./pi 100000000 seq
 3.14159
 ​
 real    0m0.108s
 user    0m0.108s
 sys     0m0.000s
 ​
 $ time ./pi 100000000 par
 3.14159
 ​
 real    0m0.028s
 user    0m0.277s
 sys     0m0.000s

默认情况下,OpenMP 尝试使用系统中可用的尽可能多的(超)线程。您可以通过环境变量 OMP_NUM_THREADS 显式指定 OpenMP 可以使用的最大线程数:

 $ time OMP_NUM_THREADS=1 ./pi 100000000 par
 3.14159
 ​
 real    0m0.111s
 user    0m0.111s
 sys     0m0.000s

Last updated